[신한 DS SW아카데미 / 1차 프로젝트] 다중 필터 구현 - MyBatis로 동적 쿼리 구성하기
[신한 DS SW아카데미 / 1차 프로젝트] 다중 필터 구현 - MyBatis로 동적 쿼리 구성하기
⭐ 해당 프로젝트의 경우, 존재하는 레퍼런스에서 약간 변형하여 팀 프로젝트로 제작 중인 신한DS 부트캠프 1차 미니 프로젝트입니다.
🤔 Intro
- 현재 진행중인 1차 프로젝트의 메인 페이지 필터 기능의 경우, 다중 선택이 가능하다.
- 다중 선택이 가능하므로, 어떤 필터가 추가되느냐에 따라 쿼리를 동적으로 구성해야 한다.
- 그러므로 MyBatis로 동적 쿼리를 조정하는 방법을 알아보자.
😀 Start!
필터 구성 전 mapper 쿼리..
1
2
3
4
5
6
7
8
9
10
11
12
13
<select id="selectAllStore" resultType="kr.co.ohgoodfood.dto.MainStore">
SELECT
s.store_id AS store_id, s.store_name AS store_name, s.store_menu AS store_menu, s.store_status AS store_status, s.category_bakery AS category_bakery, s.category_fruit AS category_fruit, s.category_salad AS category_salad, s.category_others AS category_others, s.closed_at AS closed_at,
p.product_no AS product_no, p.pickup_start AS pickup_start, p.pickup_end AS pickup_end, p.origin_price AS origin_price, p.sale_price AS sale_price, p.reservation_end AS reservation_end, p.amount AS amount,
MIN(i.store_img) AS store_img,
b.bookmark_no AS bookmark_no,
CASE WHEN b.bookmark_no IS NOT NULL THEN true ELSE false END AS bookmark
FROM Store s
JOIN Product p ON s.store_id = p.store_id
JOIN Image i ON i.store_id = s.store_id
LEFT JOIN Bookmark b ON b.store_id = s.store_id AND b.user_id = #{user_id}
GROUP BY s.store_id, s.store_name;
</select>
- 현재 main 페이지의 model에 들어가는 dto인 MainStore DTO 객체를 구성하는 쿼리이다.
- Main과 Bookmark에서 동일한 객체를 사용하기에, bookmark 여부까지 left join으로 받아오도록 구성하였고, 이를 통해 차후 main 페이지에서 북마킹 기능을 추가해야 할시, 확장이 용이하도록 구성했다.
- 여기서 중요한 수정사항은, filter에 따라 동적으로 해당 쿼리를 구성하는 것이다.
AJAX 요청 정보
- 현재 데이터 명세에 따른 AJAX Request json은 다음과 같다.
1 2 3 4 5
{ "store_status": "Y", "pickup_start": "2025-06-21", "category_bakery": "Y" }
- store_status : 예약 가능 가게만 보여주도록 한다.
- pickup_start : 이를 통해 오늘 픽업인지 내일 픽업인지를 따진다.
- “category_bakery”: “Y” : 카테고리 정보를 나타낸다.
- 현재 카테고리는 네 개로, Store 테이블에 다음과 같은 형태로 저장되어 있다.
- “category_bakery” : String
- “category_fruit”: String
- “category_salad”: String
- “category_others”: String
- 현재 카테고리는 네 개로, Store 테이블에 다음과 같은 형태로 저장되어 있다.
- 카테고리 테이블을 따로 분리하지 않은 것은, 카테고리 수가 적기도 하고, 카테고리 추가 기능까지 확장할 계획이 아직 없기 때문이다. 이 부분은 강사님의 피드백을 받아서 구성한 부분이다.
AJAX 구성 (JQuery)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 공통 AJAX 요청 함수
function sendFilterRequest() {
$.ajax({
url: '${pageContext.request.contextPath}/user/filter/store',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(filterParams),
//fragment 방식 사용, DOM을 다시 그리자니 너무 노가다라...
success: function (responseHtml) {
//로그 찍기
console.log("[AJAX 응답] 서버에서 받은 HTML:", responseHtml);
$('.productWrapper').html(responseHtml);
applyBadgeStyles(); //오퍼시티 적용
},
error: function (xhr, status, error) {
console.error("[AJAX 오류 발생]");
console.error("status:", status);
console.error("HTTP 상태 코드:", xhr.status);
console.error("응답 텍스트:", xhr.responseText);
console.error("error 객체:", error);
}
});
}
- 토글 이벤트를 통해 let filterParams = {};에 json 객체가 담기면, 이 ajax 요청 함수를 통해 Controller로 요청을 보내게 된다.
- 명세했던대로 data에 JSON 객체를 request한다.
- 원래는 JSON 객체를 받아서 화면 데이터를 변경해서 상태 변경을 시도할까 하다가…(으레 다른 프레임워크들이 그렇듯이) JSP 방식에서는 너무 한계가 있는 방식이라 그냥 fragment를 갈아 끼우는 방식을 사용하기로 하였다.
.productWrapper 영역만 갈아 끼우면 되므로, 이 영역을 지정한다.
- fragment는 따로 파일을 빼야 해서 다음과 같이 파일을 분리해서 복사해주었다.
- 파일 경로는 반드시 MvcConfig.java 설정에 있는 뷰 리졸버 경로에 맞춰준다.
UserMapper.java
1
2
3
4
5
6
7
@Mapper
public interface UserMapper {
//[user] user main 화면 & 북마크에서 보이는 가게 리스트
List<MainStore> selectAllStore(@Param("user_id") String user_id,
@Param("filter") Map<String, String> filterParams);
//.....
}
- 필터 적용으로 filterParams을 받아오게 되었으므로, 이를 @Param 애노테이션으로 묶어서 map으로 전송한다.
필터 구성 후 UserMapper.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<select id="selectAllStore" parameterType="map" resultType="kr.co.ohgoodfood.dto.MainStore">
SELECT
s.store_id AS store_id, s.store_name AS store_name, s.store_menu AS store_menu, s.store_status AS store_status, s.category_bakery AS category_bakery, s.category_fruit AS category_fruit, s.category_salad AS category_salad, s.category_others AS category_others, s.closed_at AS closed_at,
p.product_no AS product_no, p.pickup_start AS pickup_start, p.pickup_end AS pickup_end, p.origin_price AS origin_price, p.sale_price AS sale_price, p.reservation_end AS reservation_end, p.amount AS amount,
MIN(i.store_img) AS store_img,
b.bookmark_no AS bookmark_no,
CASE WHEN b.bookmark_no IS NOT NULL THEN true ELSE false END AS bookmark
FROM Store s
JOIN Product p ON s.store_id = p.store_id
JOIN Image i ON i.store_id = s.store_id
LEFT JOIN Bookmark b ON b.store_id = s.store_id AND b.user_id = #{user_id}
<where>
<if test="filter.store_status != null">
AND s.store_status = #{filter.store_status}
</if>
<if test="filter.pickup_start != null">
AND DATE(p.pickup_start) = #{filter.pickup_start}
</if>
<if test="filter.category_bakery != null">
AND s.category_bakery = #{filter.category_bakery}
</if>
<if test="filter.category_fruit != null">
AND s.category_fruit = #{filter.category_fruit}
</if>
<if test="filter.category_salad != null">
AND s.category_salad = #{filter.category_salad}
</if>
<if test="filter.category_others != null">
AND s.category_others = #{filter.category_others}
</if>
</where>
GROUP BY s.store_id, s.store_name;
</select>s.store_id, s.store_name;
</select>
- 다음처럼 IF 조건을 더해준다.
- Y인 경우만 들어오므로, null 체크만 해주도록 하자.
- MyBatis에서는 이처럼 xml 방식으로 if 문을 사용해서 동적으로 쿼리 조건을 조정할 수 있다.
UserController
- AJAX에서 /user/filter/store로 Controller 요청을 보내기로 하였으므로, 해당하는 컨트롤러를 구현해야 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13
@PostMapping("/filter/store") public String filterStoreList(@RequestBody Map<String, String> filterParams, Model model){ log.info("[log/UsersController.filterStoreList] 받은 필터 파라미터 결과 log : {}", filterParams); //임시 하드코딩 값, 실제로는 세션에서 받아온다. String user_id = "u04"; List<MainStore> mainStoreList = usersService.getMainStoreList(user_id, filterParams); model.addAttribute("mainStoreList", mainStoreList); // JSP fragment만 리턴 return "users/fragment/userMainStoreList"; }
- usersService에 구현되어 있는 getMainStoreList를 그대로 이용한다.
- 어차피 필터 조건이 붙어있는 mapper가 그대로 연결되어 있기 때문에, 그저 service를 이용해주면 된다. (MVC 구조)
- MVC 구조를 따라서 model에 객체를 담아주고 JSP fragment 링크로 return 해준다.
- 앞에서도 얘기했지만, fragment 경로는 뷰 리졸버에 있는 그대로 맞춰준다.
- 이렇게 하면 받아온 쿼리를 fragment에 연결해서 해당 fragment의 DOM 데이터를 변경하게 된다.
- AJAX에서는 지정한 부분에 변경된 fragment DOM을 붙여준다. 이를 통해 화면 상태를 변경할 수 있고, 다중 필터 구현이 가능하다:)
This post is licensed under CC BY 4.0 by the author.