서버 연동
서버 페이징은 화면만 가져가면 안 돕니다. 화면이 보내는 조건을 서버가 알아들어야 합니다.
계약은 주소 하나뿐입니다 — 화면은 자기 location.search 를 읽어 컨트롤을 복원하고, 컨트롤이 바뀌면 주소를 다시 만들어 이동합니다.
① 파라미터
| 이름 | 뜻 |
|---|---|
page / size | 페이지(1부터) / 페이지크기 |
sort / dir | 정렬 필드 / asc|desc. 단일 정렬 |
q / qf | 검색어 / 대상 필드(비면 전체) |
c_<필드> | 코드(셀렉트) 필터 |
f_<필드> / t_<필드> | 기간 시작 / 끝 |
all=1 | 엑셀용 — 조건 전체(상한까지) |
직접 눌러보세요: 완료 + 금액 내림차순 + 20개씩
② 컨트롤러
파라미터 맵을 GridQuery.of() 에 넘기고, 결과를 GridState 로 화면에 전달합니다.
private static final boolean SUM_ROW = true; // 화면별 선언 — off 면 집계 쿼리 자체를 안 돈다 @GetMapping("") public String list(@RequestParam Map<String,String> params, Model model) { GridQuery q = GridQuery.of(params); model.addAttribute("rows", mapper.findPage(q)); model.addAttribute("grid", new GridState(q, mapper.countPage(q), SUM_ROW, SUM_ROW ? mapper.sumPage(q) : null)); model.addAttribute("companyOptions", mapper.distinctCompanies()); // 코드필터 선택지는 서버가 준다 return "pages/list"; }
③ 매퍼 XML
필드 이름은 choose 를 통과한 것만 컬럼이 됩니다. 모르는 값은 기본 정렬로 떨어집니다 — 주소창 값이 SQL 로 새지 않습니다.
<sql id="pageWhere">
WHERE t.delete_yn = 'N'
<if test="qLike != null">
AND (
<choose>
<when test="qf == 'company'">t.company ILIKE #{qLike}</when>
<otherwise>t.company ILIKE #{qLike} OR t.note ILIKE #{qLike}</otherwise>
</choose>
)
</if>
<if test="code.status != null">AND t.status = #{code.status}</if>
<if test="from.accrualYm != null">AND t.accrual_ym >= #{from.accrualYm}</if>
</sql>
<select id="findPage" resultType="…">
SELECT <include refid="cols"/> FROM tax_invoice t
<include refid="pageWhere"/>
ORDER BY
<choose>
<when test="sort == 'company'">t.company</when>
<when test="sort == 'amount'">t.target_amount</when>
<!-- 화이트리스트에 없는 이름은 여기로 떨어진다 -->
<otherwise>t.accrual_ym</otherwise>
</choose>
<choose><when test="dir == 'asc'">ASC NULLS FIRST</when><otherwise>DESC NULLS LAST</otherwise></choose>, t.id DESC
LIMIT #{size} OFFSET #{offset}
</select>
<select id="sumPage" resultType="map">
<!-- 별칭에 따옴표 필수 — map 결과는 카멜 변환이 안 된다 -->
SELECT COALESCE(SUM(t.target_amount),0) AS "amount" FROM tax_invoice t <include refid="pageWhere"/>
</select>
④ 일괄 저장 — 화이트리스트가 생명
수정모드는 (id, field, value) 목록을 보냅니다. 필드명을 검사하지 않으면 delete_yn·create_id 까지 바뀝니다.
private static final List<String> ALLOWED = List.of("company","owner","title","amount","status"); for (Map<String,Object> u : updates) { if (!ALLOWED.contains(String.valueOf(u.get("field")))) return Map.of("ok", false, "message", "허용되지 않은 항목입니다"); }
새 열을 편집 가능하게 열 때마다 이 목록에도 추가해야 합니다. 안 하면 화면에선 고쳐지는데 저장이 조용히 거절됩니다.