Next.js + Ag Grid로 관리자 페이지 만들기 (1)

jinvicky·2025년 2월 9일
0

Intro


사이트를 임시 오픈하고 신청서를 받기 시작했다. 문제는 관리자 페이지였다.
처음엔 아래처럼 생각했는데 내 사이트가 소규모도 아니고 초소규모일 것 같은데 그냥 한 페이지에 퉁쳐버릴까? 라고 생각했는데 역시 안되겠다.

  • 사용자랑 관리자가 같이 있으니까 인터셉터 조건이나 헤더 조건이 안 맞는다.
  • 관리자는 모든 데이터를 조회할 수 있는데 그런 api가 감춰져 있더라도 사용자 사이트에서 호출 가능하다는 것이 보안상 큰일나겠다 생각이 들었다.
  • 그래서 그냥 관리자는 백/프론트 따로 개발하고 로컬로만 동작시키기로 했다.

여하튼 지금 결제 요청서랑 결제 건은 쌓여만 가고 있다. 관리자가 시급하다~! 라이브러리의 힘으로 빨리 구현해보자.

Ag Grid

관리자 페이지에 제격인 라이브러리라고 생각한다. Next.js 13 이상(App Router)으로 진행해 본다.

npm install ag-grid-react

블로그에서 본 걸로는 ag-grid-react랑 ag-grid-community를 같이 깔고 이 두 lib의 버전이 동일해야만 한다고 한다.

마지막에 시도했을 때는 layout.tsx에 css 파일 2개 import만하는 거였는데 그 사이 변화가 생긴 듯 하다. (https://velog.io/@jinvicky/cms-update-240912)

AG Grid: error #239 Theming API and CSS File Themes are both used in the same page. In v33 we released the Theming API as the new default method of styling the grid. See the migration docs https://www.ag-grid.com/react-data-grid/theming-migration/. Because no value was provided to the theme grid option it defaulted to themeQuartz. But the file (ag-grid.css) is also included and will cause styling issues. Either pass the string "legacy" to the theme grid option to use v32 style themes, or remove ag-grid.css from the page to use Theming API.

아래 css import를 주석처리하고 ModuleRegistry로 등록을 추가하면서 에러를 해결했다.
ModuleRegistry를 전역으로 1번만 등록하고 싶었는데 layout.tsx에 등록하는 것으로 해결하지 못했다.

사용 페이지별로 아래 코드를 추가한다.

// 이거 이제 안 쓰는 듯.
// import "ag-grid-community/styles/ag-grid.css";
// import "ag-grid-community/styles/ag-theme-alpine.css";

import { AllCommunityModule, ModuleRegistry } from 'ag-grid-community'; 
ModuleRegistry.registerModules([AllCommunityModule]);

참고 👉 https://www.ag-grid.com/react-data-grid/modules/

간단 List 구현하기

mui를 사용할 수도 있지만 코드 라인 수와 간편함이 차원이 다르다:)

상위 서버 컴포넌트에서 데이터를 fetch하고 하위 클라이언트 컴포넌트에 rowData를 뿌려준다. 서버 컴포넌트라서 컴포넌트 자체에 async를 추가할 수 있다.

CommissionApplyListPage.tsx

import CommissionApplyList from "./CommissionApplyList";

const CommissionApplyListPage = async () => {
    const resp = await fetch(process.env.NEXT_DOMAIN_URL + "/api/commission/apply");
    const data = await resp.json() as ApiResult<CommissionApply[]>;

    return <>
        <CommissionApplyList rowData={data.data} />
    </>
};

export default CommissionApplyListPage;

CommissionApplyList.tsx

"use client";
import React, { useState } from "react";

import Link from "next/link";

import { AllCommunityModule, ModuleRegistry } from 'ag-grid-community';
import { AgGridReact } from "ag-grid-react";
import { ColDef } from "ag-grid-community";

ModuleRegistry.registerModules([AllCommunityModule]);

interface CommissionApplyListProps {
  rowData: CommissionApply[];
}

const CommissionApplyList = ({ rowData }: CommissionApplyListProps) => {

  const agGridReactRef = React.useRef<AgGridReact>(null);

  const [columnDefs] = useState<ColDef[]>([
    {
      field: "id",
      checkboxSelection: true,
      cellRenderer: (params: any) => (
        <Link href={`/commission/apply/${params.data.id}`}>{params.value}</Link>
      ),
    },
    { field: "userName" },
    { field: "userEmail" },
    {
      field: "title",
      cellRenderer: (params: any) => (
        <Link href={`/commission/apply/${params.data.id}`}>{params.value}</Link>
      ),
    },
    { field: "status" },
    { field: "rgtrDt" }
  ]);

  const gridOptions = {
    columnDefs: columnDefs,
    pagination: true,
    paginationPageSize: 10,
  };

  const handleSelected = () => {
    if (agGridReactRef.current) {
      const selectedNodes = agGridReactRef.current.api.getSelectedNodes();
      const selectedData = selectedNodes.map((node) => node.data);
      console.log(selectedData);
    }
  }

  return (
    <div>
      <div className="ag-theme-alpine" style={{ height: '600px', width: '100%' }}>
        <AgGridReact
          ref={agGridReactRef}
          rowData={rowData}
          columnDefs={columnDefs}
          pagination={gridOptions.pagination}
          paginationPageSize={gridOptions.paginationPageSize}
          rowSelection={'multiple'}
        />
      </div>
      <div className="p-4">
        <button
          onClick={handleSelected}
          className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
        >
          삭제하기
        </button>
      </div>
    </div>

  );
};

export default CommissionApplyList;
  • field는 데이터의 key값과 일치해야 한다.
  • 단순 텍스트가 아닌 링크, 또는 컴포넌트를 위해서는 renderer에 정의한다.
  • rowSelection은 다중 row 선택을 가능케 하며, checkboxSelectiontrue로 하면 체크박스가 생성된다.
  • handleSelected 함수를 참고하면 체크된 rowData들을 useRef로 얻어올 수 있다. 나는 다중 삭제에 적용해볼 예정이다.

Reference

ag-grid 기본 정리 굿굿 https://jforj.tistory.com/255

profile
Front-End와 Back-End 경험, 지식을 공유합니다.

0개의 댓글

관련 채용 정보