저번시간에 리뷰기능을 구현하면서 마지막부분에 매장 상세화면에서 리뷰가 몇개인지
각각의 별점이 몇개인지 평균 별점이 얼마인지를 나타내도록 했었습니다
이번 시간에는 매장상세화면이 아닌 매장목록화면에서 각 매장들의 평균 별점과 리뷰의 숫자를
나타내도록 할것이며 여기에 현재 가게가 오픈상태인지 아닌지를 보여줄겁니다
이미 지난시간에 StoreDto도 수정을 했고 매장목록에서 평균별점과 리뷰수, 매장오픈상태
만 추가되는것이기에 기존의 매장목록 쿼리만 수정하면 됩니다
기존 StoreMapper.xml의 storeList쿼리를 다음과 같이 변경해주세요
<select id="storeList" resultType="com.han.delivery.dto.StoreDto">
WITH R_COUNT AS (
SELECT STORE_ID
,ROUND(AVG(SCORE), 1) SCORE
,COUNT(REVIEW_CONTENT) REVIEW_COUNT
,COUNT(BOSS_COMMENT) BOSS_COMMENT_COUNT
FROM DL_REVIEW
GROUP BY STORE_ID
),
STORE AS (
SELECT S.*,
T.*
FROM DL_STORE S
LEFT JOIN R_COUNT T
ON S.ID = T.STORE_ID
WHERE CATEGORY = #{category}
AND STORE_ADDRESS1 LIKE '${address}%')
SELECT * FROM
(SELECT ROWNUM RN,
RESULT.*
FROM
(SELECT C.*
,'true' IS_OPEN
FROM STORE C
WHERE TO_CHAR(SYSTIMESTAMP, 'HH24') BETWEEN OPENING_TIME AND CLOSING_TIME
UNION ALL
SELECT C.*
,'false' IS_OPEN
FROM STORE C
WHERE TO_CHAR(SYSTIMESTAMP, 'HH24') NOT BETWEEN OPENING_TIME AND CLOSING_TIME
) RESULT
)
쿼리에 대해서 간단히 설명하자면 우리가 필요한 정보는 Store의 모든 정보와
평균별점, 리뷰수, 사장님댓글수, 현재오픈상태 입니다. 현재오픈상태를 제외한
나머지 정보들만을 가져오기는 어렵지 않습니다. 하지만 현재오픈상태를 나타내기 위해서는
쿼리가 많이 복잡해지게 됩니다.
이 때 몇가지 방법이 있는데 현재오픈상태를 제외한 나머지 데이터만 DB에서 서버로 가져오고
서버에서 for문을 돌려 현재 시간이 오픈시간과 마감시간 사이에 존재하면 변수에 true를
아니면 false를 넣어주면 됩니다 List이므로 많은 for문이 돌아가지만
단일 for문이기에 그리 많은 시간이 걸리진 않습니다
두번째 방법은 Dto에 담긴 데이터를 view로 넘길때 현재 시간을 model에 심어
jstl의 c:if문을 사용하는 방법입니다. 우리가 view로 넘긴 List는 어차피
jstl의 foreach문을 통해 반복하여 뿌려지게 되므로 이때 오픈상태를 판별하면
첫번째 방법처럼 쓸데 없이 for문을 한번 더 돌릴 필요가 없어집니다
세번째 방법은 위의 쿼리처럼 DB에서 오픈상태를 판별하는 방법입니다 이미 DB에서
판별을 끝내고 데이터가 서버로 넘어가기 때문에 서버측에서 로직을 추가할 필요도 없고
view를 수정할 필요도 없습니다 다만 쿼리가 복잡해지게 됩니다
일단 여기서는 세번째 방법으로 쿼리를 구성하였고 쿼리를 보면 with절을 사용하였는데
with절이란 임시 테이블을 만든다는 관점에서 view와 쓰임새가 비슷합니다 다만
view는 drop할때까지 없어지지 않지만 with절은 정의되어 있는 쿼리문 안에서만 실행된다는
차이점이 있습니다. 제일 처음 리뷰테이블에서 매장번호, 평균별점, 리뷰수, 사장님댓글수를
계산하여 R_COUNT라는 이름으로 임시테이블을 하나 생성합니다 그후 이 임시테이블과
매장테이블을 합친후 다시 STORE라는 이름으로 임시테이블을 생성합니다
이 임시테이블에는 매장의 현재오픈상태 한가지가 존재하지 않습니다. 따라서
이렇게 만든 임시테이블에서 현재시간이 오픈시간과 마감시간 사이에 있으면 IS_OPEN
이란 이름으로 true를 아니면 false를 저장하고 UNION ALL을 통해 다시 두케이스를
하나로 합쳐줍니다 이때 ROWNUM를 넣는 이유는 페이징 기능을 추가하기 위해서입니다
이제 store-li.jsp에 기존에 주석처리 되어 있던 부분을 다 주석해제 해주시면
다음과 같이 평균 별점과 리뷰수, 사장님댓글 수와 현재 영업상태를 알 수 있습니다
현재 매장목록에서는 내 주위에 있는 매장정보를 한번에 다 가져오게 되어있습니다
만약 내 주위에 매장이 수천개라면 수천개의 매장정보를 다 가져와서 화면에 뿌려줘야
하므로 로딩시간이 길어지게 됩니다. 이를 방지하기 위하여 처음에는 10개의 매장을
뿌려주고 그 이후 사용자가 스크롤을 바닥근처까지 내릴시에 추가적으로 10개의 매장을
다시 뿌려주는식으로 구현할겁니다 이를 위해 utils패키지에 클래스를 하나 추가합니다
@Data
public class Page {
private int view = 10; // 화면에 출력할 목록 수
private int firstList; // 페이지 첫번째 목록
private int lastList; // 페이지 마지막 목록
public Page() {
this(1);
}
public Page(int movePage) {
page(movePage, view);
}
public Page(int movePage, int view) {
page(movePage, view);
}
public void page(int movePage, int view) {
this.firstList = (view * movePage) - view + 1;
this.lastList = movePage * view;
}
}
처음에는 firstList =1 , lastList=10으로 우리가 쿼리에서 설정한 ROWNUM을 기준으로
1~10번까지의 데이터를 가져오고 사용자가 스크롤을 바닥까지 내릴시 movePage가 2로
변하여 11~20번까지의 데이터를 가져오게 됩니다
//매장목록 페이징
@GetMapping("/api/store/storeList")
public ResponseEntity<List<StoreDto>> sortStore(int category, int address1, String sort, int page) {
List<StoreDto> storeList = storeService.storeList(category, address1 / 100, sort, page);
return ResponseEntity.ok().body(storeList);
}
우리는 사용자가 스크롤을 밑으로 내릴때마다 새로운 매장정보를 동적으로 화면에
이어붙여줘야 합니다. 따라서 Ajax를 사용해 비동기 요청을 통해 데이터를 가져올것이며
이때 sort는 배달비순, 별점순, 리뷰순과 같이 특정 조건에 따라 데이터를 정렬할수
있도록 문자열을 저장합니다
//수정
@Transactional
public List<StoreDto> storeList(int category, int address){
return storeList(category, address, "기본 순", 1);
}
//추가
@Transactional
public List<StoreDto> storeList(int category, int address1, String sort, int page) {
Page p = new Page(page, 10);
Map<String, Object> map = new HashMap<>();
map.put("category", category);
map.put("address1", address1);
map.put("firstList", p.getFirstList());
map.put("lastList", p.getLastList());
map.put("sort", sort);
System.out.println("페이지 시작 = " + p.getFirstList() + " 페이지 끝 = " + p.getLastList());
return storeMapper.storeList(map);
}
기존에 매장목록에서는 동기적으로 한번에 데이터를 가져와 화면에 뿌려줬었습니다
하지만 페이징의 경우 동기+비동기가 둘다 사용됩니다
처음 매장목록화면에 접근시 Controller를 통해 1~10번째 데이터가 동기적으로 뿌려지지만 그 이후 11번째 데이터부터는 apiController를 통해 비동기적으로 뿌려지게 됩니다
따라서 첫 데이터는 page에 1을 할당하여 1~10번째 데이터를 가져오게 해줍니다
public List<StoreDto> storeList(Map<String, Object> map);
<select id="storeList" resultType="com.han.delivery.dto.StoreDto">
WITH R_COUNT AS (
SELECT STORE_ID
,ROUND(AVG(SCORE), 1) SCORE
,COUNT(REVIEW_CONTENT) REVIEW_COUNT
,COUNT(BOSS_COMMENT) BOSS_COMMENT_COUNT
FROM DL_REVIEW
GROUP BY STORE_ID
),
STORE AS (
SELECT S.*,
T.*
FROM DL_STORE S
LEFT JOIN R_COUNT T
ON S.ID = T.STORE_ID
WHERE CATEGORY = #{category}
AND STORE_ADDRESS1 LIKE '${address}%'
<if test="sort == '배달 빠른 순'">
ORDER BY DELIVERY_TIME
</if>
<if test="sort == '배달팁 낮은 순'">
ORDER BY DELIVERY_TIP
</if>
<if test="sort == '별점 높은 순'">
ORDER BY SCORE
</if>
<if test="sort == '리뷰 많은 순'">
ORDER BY REVIEW_COUNT
</if>
<if test="sort == '최소 주문 금액 순'">
ORDER BY MIN_DELIVERY
</if>
)
SELECT * FROM
(SELECT ROWNUM RN,
RESULT.*
FROM
(SELECT C.*
,'true' IS_OPEN
FROM STORE C
WHERE TO_CHAR(SYSTIMESTAMP, 'HH24') BETWEEN OPENING_TIME AND CLOSING_TIME
UNION ALL
SELECT C.*
,'false' IS_OPEN
FROM STORE C
WHERE TO_CHAR(SYSTIMESTAMP, 'HH24') NOT BETWEEN OPENING_TIME AND CLOSING_TIME
) RESULT
)
WHERE RN BETWEEN #{firstList } AND ${lastList }
</select>
if문을 통해 sort에 저장된 문자열을 비교하여 특정 조건에 따라 정렬을 바꿔줍니다
마지막에는 WHERE절을 추가하여 page가 증가함에 따라 새로운 데이터를 가져오도록
해줬습니다 이제 매장 목록에서 스크롤을 내릴시 계속해서 새로운 데이터를 가져옵니다