스프링부트+jsp로 배달사이트 만들기-25 매장검색

2021. 12. 11. 19:12스프링부트

store 폴더 아래에 jsp, css, js를 추가합니다

search.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ include file="/WEB-INF/view/include/link.jsp" %>
<link rel="stylesheet" href="/css/layout/nav.css" >
<link rel="stylesheet" href="/css/store/search.css" >
<link rel="stylesheet" href="/css/store/store-li.css">
<%@ include file="/WEB-INF/view/include/header.jsp" %>



	<main>
	  	<form action="/store/search" method="get" onsubmit="return check()" >
		  	<div class="input_box">
			  	<div>
					<label for="submit">
						<i class="fas fa-search"></i>
					</label>
					<input type="submit" id="submit">
				</div>
				<div>	
					<input type="text" class="search" name="keyword" maxlength="33" value="${keyword }" placeholder="어떤 가게를 찾으시나요?" autofocus >
					<div class="info">현재 주소지를 기준으로 검색됩니다.</div>
					<input type="hidden" value="${BMaddress.address1 }" name="address1" id="deleveryAddress1">
					<%@ include file="/WEB-INF/view/include/modifyAddress.jsp" %> 
				</div>
				<div>
					<button type="button" class="word_delete"><i class="fas fa-times"></i></button>
				</div>
			
			</div>
		</form>
		
		<div class="search_word_head">
			<h2>최근 검색어</h2>
			<button>전체삭제</button>
		</div>
		<div class="search_word">
		
			<ul>
				<c:if test="${!empty keywordList }">
				<c:forEach items="${keywordList }" var="keywordList">
				
					<li>
						<span>${keywordList }</span>
						<button><i class="fas fa-times"></i></button>
					</li>
					
				</c:forEach>
				</c:if>
			</ul>
		</div>
		
		<div class="box">
			<c:if test="${noSearch }"> 
				<div class="no_search">검색 결과가 없습니다</div>
			</c:if>
			
			<ul class="store">
			<c:forEach items="${storeList }" var="storeList" >
				<%@ include file="/WEB-INF/view/store/store-li.jsp" %>
			</c:forEach>
                    
			</ul>
		</div>
            	
	</main>

  
	<%@ include file="/WEB-INF/view/include/nav.jsp" %>

	<%@ include file="/WEB-INF/view/include/footer.jsp" %>
  

  	<script type="text/javascript" src="/js/store/search.js" ></script>
  
</body>
</html>

 

search.css

main {
	width: 100%;
	max-width: 1200px;
	margin: 0 auto;
	min-height: calc(100vh - 312px);
}

main form {
	margin: 20px auto 50px;
}

main form .input_box {
	width: 50%;
	height: 45px;
	background: #F6F6F6;
	border-radius: 10px;
	margin: 0 auto;
	display: flex;
}

main form .input_box>div:first-child {
	width: 50px;
	display: flex;
	align-items: center;
}

main form .input_box>div:nth-child(2) {
	width: 100%;
}

main form .input_box>div:nth-child(3) {
	width: 50px;
	display: flex;
	align-items: center;
	justify-content: center;
}

/* 검색창 텍스트 지우기버튼 */
main form .input_box>div:nth-child(3) button {
	width: 20px;
	height: 20px;
	border: none;
	color: #fff;
	background: #999999;
	border-radius: 50%;
	display: none;
}

/* main form .input_box>div:nth-child(3) button i {
	position: relative;
	top: 1px;
} */

main form input[type=text] {
	height: 100%;
	width: 100%;
	font-size: 1.8rem;
	background: none;
	border: none;
	padding-left: 5px;
}

main .info {
	text-align: center;
	font-weight: bold;
	margin-top: 5px;
}

.search_word_head {
	margin-left: 15px;
}

.search_word {
	margin-bottom: 40px;
	box-shadow: 0px 2px 3px 0px rgb(0 0 0/ 25%);
	border-bottom: 1px solid #ddd;
}

i.fa-search {
	font-size: 1.8rem;
	color: #999999;
	margin-left: 15px;
}

main form input[type=submit] {
	display: none;
}

.search_word_head {
	display: flex;
	margin-bottom: 5px;
}

.search_word_head button {
	border: none;
	border-radius: 20px;
	background: #F6F6F6;
	padding: 5px 10px 5px 10px;
	margin-left: 10px;
}

.search_word ul {
	width: 98%;
	margin: 10px auto;
	white-space: nowrap;
	overflow-x: auto;
	overflow-y: hidden;
}

.search_word ul::-webkit-scrollbar {
	background: none;
}

.search_word li {
	border-radius: 20px;
	background: #ECFAFA;
	margin: 0 5px 5px 5px;
	padding: 5px 10px 5px 10px;
	overflow-x: auto;
	display: inline-block;
}

.search_word li span, .search_word li i {
	color: #30A9A6;
	cursor: pointer;
}

.search_word li button {
	border: none;
	background: none;
	margin-left: 15px;
}

.no_search {
	font-size: 2.3rem;
    text-align: center;
}




@media ( max-width :1024px) {
	main {
		width: 100%;
	}
	main form .input_box {
		width: 70%;
	}
	.box .store li {
		width: 100%;
		margin: 20px auto 0px;
		border: none;
		border-radius: 0;
		border-bottom: 2px solid #ddd;
	}
}

@media ( max-width :767px) {
	main {
		min-height: calc(100vh - 247px);
	}
	main form .input_box {
		width: 95%;
	}
	main form .input_box>div:first-child {
		width: 40px;
	}
	i.fa-search {
		font-size: 1.6rem;
	}
	.search_word {
		margin-bottom: 15px;
	}
}

 

search.js


inputCheck();

$("input[name='keyword']").keyup(function(key){
    inputCheck();
    
    // 모바일 검색버튼
    if(key.keyCode == 13) {
        $("#submit").click();
    }
})	


// 검색창 텍스트 지우기버튼
$(".word_delete").click(function(){
    $("input[name='keyword']").val("");
    
    inputCheck();
})


// 검색어 전체삭제
$(".search_word_head button").click(function(){
        $.ajax({
        url : "/store/keyword-all",
        type : "DELETE",
        success : function(){
            $(".search_word li").css("display" , "none");
            
                
        } // success
    }); // ajax
})





// 검색어 1개 삭제
$(document).on("click", ".search_word li button", function(){ 
    const keyword = $(this).siblings().text();
    const index = $(this).parent("li").index();
    
    $.ajax({
        url : "/store/keyword-one",
        type : "DELETE",
        data : {keyword : keyword},
        success : function(){
            $(".search_word li").eq(index).remove();
        } // success
    }); // ajax
})





//	최근 검색어 클릭시 재검색
$(document).on("click" ,".search_word span" , function(){
    
    $(".search").val($(this).text());
    
    $("#submit").click();
    
    inputCheck();
    
})
    
function inputCheck(){
    $("input[name='keyword']").val() == "" ? 
            $(".word_delete").css("display" , "none") : $(".word_delete").css("display" , "block") 
}
    

function check() {
    const keyword = $(".search").val().replaceAll(" ","");
        
    if(keyword == "" ) {
        return false;
    }
    
    if($("#deleveryAddress1").val() == "" ) {
        modifyAddress();
        swal({
            title: "주소를 입력해주세요",
            text: "현재 주소지를 기준으로 검색됩니다." 
        });
        return false;
    }
    
    return true;
}

 

StoreController

@GetMapping("/store/search")
public String search(Integer address1, String keyword, Model model) {

    if(keyword != null) {
        // 쿠키저장
        // db 불러오기
        
    }

    return "store/search";
}

 

keyword가 없을땐 빈 페이지를 보여주고 있으면 검색어를 쿠키에 저장 후 db에서 가게 이름에 keyword가 포함된 가게목록을 불러옵니다

먼저 이전에 만든 CookieManager클래스에 검색어를 저장할 메서드를 추가합니다

public LinkedHashSet<String> saveKeyword(String keyword) throws Exception {
    final String KEYWORD = "KEYWORD";
    final int LIST_SIZE = 5;
    
    String keywordList = findCookie(KEYWORD);
    
    LinkedHashSet<String> set = new LinkedHashSet<>();
    
    if(keywordList == null) {
        set.add(keyword);
        addCookie(KEYWORD, set.toString());
        return set;
    } 
    
    set.add(keyword);
    
    StringTokenizer st = new StringTokenizer(keywordList, ", ");
    
    while(st.hasMoreTokens() && set.size() < LIST_SIZE) {
        String key = st.nextToken();
        set.add(key);
    }
    addCookie(KEYWORD, set.toString());
    
    return set;
}

이전에 만든 likes메서드와 비슷하지만 여기서는 

중복된 검색어는 저장하지 않고 순서를 유지하기 위해 LinkedHashSet을 사용했습니다 

그리고 이 목록을 화면에 출력하기 위해 LinkedHashSet을 반환합니다

 

컨트롤러에서 메서드를 실행합니다

@GetMapping("/store/search")
public String search(Integer address1, String keyword, Model model) throws Exception {

    CookieManager cm = new CookieManager();
    if(keyword != null) {
        LinkedHashSet<String> keywordList = cm.saveKeyword(keyword);
        System.out.println("검색어 목록 : " + keywordList);
        model.addAttribute("keywordList", keywordList);
    } else {
        String[] keywordList = cm.findCookie("KEYWORD").split(", ");
        model.addAttribute("keywordList", keywordList);
    }

    return "store/search";
}

검색어가 있을 땐 retrurn된 set을 화면에 출력하고, 없을 땐 쿠키에 저장된 list문자열을 배열로 변환해 출력합니다

 

 

db에서 검색목록을 가져올 수 있게 수정합니다

 

StoreController

@GetMapping({"/store/search", "/store/search/{page}"})
public String search(Integer address1, String keyword, @PathVariable(required = false) Integer page, Model model) throws Exception {

    CookieManager cm = new CookieManager();
    if(keyword != null) {
        LinkedHashSet<String> keywordList = cm.saveKeyword(keyword);
        model.addAttribute("keywordList", keywordList);
        
        Page p = new Page(page);
        List<Store> storeList = storeService.storeSearch(keyword, address1 / 100, p);
        model.addAttribute("keyword", keyword);
        
        if(storeList.size() == 0) {
            model.addAttribute("noSearch", true);
        } else {
            p.totalPage(storeList.get(0).getListCount());
            model.addAttribute("page", p);
            model.addAttribute("storeList", storeList);
        }
    } else {
        String key = cm.findCookie("KEYWORD");
        if(key != null) {
            String[] keywordList = key.split(", ");
            model.addAttribute("keywordList", keywordList);
        }
    }

    return "store/search";
}

페이징을 하기위해 매핑을 하나 더 추가했고 

검색어와 페이지를 같이 화면에 전달합니다

 

Store클래스에 매장 수를 받을 필드를 추가합니다

private int listCount;	// 매장 수

 

 

StoreService

List<Store> storeSearch(String keyword, int address, Page p);

 

StoreServiceImp

@Override
public List<Store> storeSearch(String keyword, int address, Page p) {
    Map<String, Object> map = new HashMap<>();
    map.put("keyword", keyword);
    map.put("address", address);
    map.put("firstList", p.getFirstList());
    map.put("lastList", p.getLastList());
    
    return storeDAO.storeSearch(map);
}

 

StoreDAO

List<Store> storeSearch(Map<String, Object> map);

 

StoreDAOImp

	@Override
	public List<Store> storeSearch(Map<String, Object> map) {
		return sql.selectList("store.storeSearch", map);
	}

 

StoreMapper

<select id="storeSearch" resultType="Store">
    WITH R_COUNT AS (
       SELECT  R.STORE_ID 
               ,ROUND(R.SCORE, 1) SCORE
               ,R.REVIEW_COUNT
               ,R.BOSS_COMMENT_COUNT 
       FROM 
               (SELECT STORE_ID
                       ,AVG(SCORE) SCORE
                       ,COUNT(REVIEW_CONTENT) REVIEW_COUNT
                       ,COUNT(BOSS_COMMENT) BOSS_COMMENT_COUNT 
               FROM    BM_REVIEW GROUP BY STORE_ID ) R  
   ),
   STORE AS (
       SELECT  	S.*
                   ,T.*
                   ,CASE WHEN MOD(24 - S.OPENING_TIME + S.CLOSING_TIME, 24) != 0 THEN MOD(24 - S.OPENING_TIME + S.CLOSING_TIME, 24) ELSE 24 END BS_TIME
       FROM		BM_STORE S
       LEFT JOIN 	R_COUNT T
       ON	 		S.ID = T.STORE_ID
       WHERE       STORE_ADDRESS1 LIKE '${address}%'
       AND         STORE_NAME LIKE '%${keyword}%'
   )
   
   SELECT * FROM 
       (SELECT ROWNUM RN,
               COUNT(*) OVER() list_count, 
               RESULT.* 
       FROM   
               (SELECT C.* 
                       ,'true' IS_OPEN 
               FROM    STORE C  
               WHERE   TO_CHAR(SYSTIMESTAMP, 'HH24') BETWEEN OPENING_TIME AND OPENING_TIME + BS_TIME
               
               UNION ALL
                
               SELECT C.*
                      ,'false' IS_OPEN 
               FROM   STORE C  
               WHERE  TO_CHAR(SYSTIMESTAMP, 'HH24') NOT BETWEEN OPENING_TIME AND OPENING_TIME + BS_TIME 
               ) RESULT
        ) 
   WHERE RN BETWEEN #{firstList } AND ${lastList }	 
       
   </select>

 

 

 

 

 

 

jsp에 pageBox.jsp를 추가합니다

 <c:if test="${!empty storeList }"> 
 	<%@ include file="/WEB-INF/view/include/pageBox.jsp" %>
 </c:if>

 

 

페이지 버튼은 생겼지만 페이지를 이동하게되면 쿼리스트링이 사라지게 됩니다

 

${requestScope['javax.servlet.forward.query_string']}를 이용하면 현재 쿼리스트링을 가져올 수 있습니다

쿼리스트링을 가져올 수 있게 pageBox를 수정합니다

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fm" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<style>
	
.page_box {display: flex; justify-content: center; margin: 20px 0; }

.page_box li { border: 1px solid #999; border-right: none; width: 35px; height: 35px; text-align: center; line-height: 35px; }

.page_box li:last-child { border-right: 1px solid #999; }

.page_box li a { display: block; width: 100%; height: 100%; }

.now_page { background: #30DAD9; color: #fff; cursor: default; }

.now_page:hover { color: #fff; }

@media(max-width :767px) {
	.page_box { margin-top: 20px; }
	.page_box li { width: 25px; height: 25px; line-height: 25px; font-size: 12px; }
}	
</style>
<c:set var="queryString" value="${requestScope['javax.servlet.forward.query_string']}" />
<c:if test="${!empty queryString}"> 
	<c:set var="queryString" value="${'?'}${queryString}" />
</c:if>

<c:set var="uri" value="${requestScope['javax.servlet.forward.request_uri']}" />
<c:set var="pathValiable" value="${'/' }${page.nowPage }" />
<c:set var="path" value="${fn:replace(uri, pathValiable, '') }${'/' }" /> 


<ul class="page_box">
	<c:if test="${page.pageCount < page.firstPage }">
        <li><a href="${path }${page.prevPage }${queryString }">이전</a></li>
    </c:if>
    <c:forEach begin="${page.firstPage }" end="${page.firstPage + page.pageCount - 1 }" var="i">
    	<c:if test="${i <= page.totalPage}">
           <c:if test="${i != page.nowPage }">
               <li><a href="${path }${i }${queryString }">${i }</a></li>
           </c:if>
           <c:if test="${i == page.nowPage }">
               <li><a class="now_page" onclick="return false;" href="${path }${i }${queryString }">${i }</a></li>
           </c:if>
         </c:if> 
    </c:forEach>
    <c:if test="${page.firstPage + page.pageCount <= page.totalPage }">
        <li><a href="${path }${page.nextPage }${queryString }">다음</a></li>
    </c:if>
</ul>

 

 

페이지 이동 시에도 쿼리스트링을 유지 할 수 있게되었습니다

 

매장상세페이지로 이동할수 있게 store-li.jsp를 수정합니다

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fm" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
    
    <c:set var="uri" value="${requestScope['javax.servlet.forward.request_uri']}" />
    <c:choose>
	    <c:when test="${fn:split(uri, '/')[0] == 'admin'}">
	    	<c:set var="path" value="/admin/management" />
	    </c:when>
	    <c:otherwise>
	    	<c:set var="path" value="/store" />
	    </c:otherwise>
    </c:choose>
    
    
    
    
<li>
	<div class="img_box">
		<a href="${path }/detail/${storeList.id }"><img src="${storeList.storeImg }" alt="이미지"></a>
	</div>

	<div class="info_box">
	
		<h2><a href="${path }/detail/${storeList.id }">${storeList.storeName }</a></h2>
		
		<a href="${path }/detail/${storeList.id }">
			<span>
				<span>평점 ${storeList.score }</span>
				
				<span class="score_box">
					<c:forEach begin="0" end="4" var="i">
						<c:if test="${Math.round(storeList.score) > i }">
							<i class="far fas fa-star"></i>
						</c:if>
						<c:if test="${Math.round(storeList.score) <= i }">
							<i class="far fa-star"></i>
						</c:if>
					</c:forEach>
				</span>
			</span>
			
		<span>
			<span>리뷰 ${storeList.reviewCount }</span>
			<span>사장님 댓글 ${storeList.bossCommentCount }</span>
		</span>
		
		<span>
			<span>최소주문금액 <fm:formatNumber value="${storeList.minDelevery }" pattern="###,###" />원</span>
			<span>배달팁 <fm:formatNumber value="${storeList.deleveryTip }" pattern="###,###" />원</span>
		</span>
		<span>배달시간 ${storeList.deleveryTime }분</span>
		</a>
	</div>
		
	<c:if test="${!storeList.isOpen}">
		<div class="is_open">
			<a href="/store/detail/${storeList.id }">지금은 준비중입니다</a>
		</div>
	</c:if>
</li>