colon-grid v1.1.0

서버 연동

서버 페이징은 화면만 가져가면 안 돕니다. 화면이 보내는 조건을 서버가 알아들어야 합니다.
계약은 주소 하나뿐입니다 — 화면은 자기 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 &gt;= #{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", "허용되지 않은 항목입니다");
}
새 열을 편집 가능하게 열 때마다 이 목록에도 추가해야 합니다. 안 하면 화면에선 고쳐지는데 저장이 조용히 거절됩니다.