codedang icon indicating copy to clipboard operation
codedang copied to clipboard

feat(fe): poc - new data table for admin

Open eunnbi opened this issue 4 months ago • 2 comments

Description

DataTableAdmin 컴포넌트의 문제

  • 특정한 페이지에서만 사용하는 기능들이 섞여 있음 (예: 문제 임포트 기능, 대회 복사 기능)
    • 해결: 특정한 곳에서만 사용하고 있는 기능들은 밖으로 빼서 분리하기
  • 페이지를 기준으로 한 로직 분기가 많다.
    • 삭제 기능에서 문제 페이지인지 대회 페이지인지 분기하여 처리함
    • 디테일 페이지로 이동할 때 각 페이지별로 이동 링크가 다른데 이걸 다 내부에서 분기하여 처리함
    • 해결: 분기 처리를 없애고 prop으로 처리하기
  • Loading UI를 제공하지 않아 DataTableAdmin 사용할 때마다 loading ui를 직접 만들어야 함 -> 중복 발생
    • 해결: Loading UI 컴포넌트를 제공해 중복 없애기

컴포넌트 구조 (@/app/admin/_components/table)

  • DataTableRoot
    • DataTableSearchBar
    • DataTableLevelFilter
    • DataTableLangFilter
    • DataTableProblemFilter
    • DataTableDeleteButton
    • DataTable
    • DataTablePagination
  • DataTableColumnHeader
  • DataTableFallback
  • 특정 기능을 사용하고 싶다면 직접 컴포넌트를 가져다 쓰는 구조로 변경
    • 컴포넌트 분리를 통해 한 컴포넌트에 있던 prop들이 어느 곳에서 사용되는지, 어떤 목적의 prop인지 명확해짐.
    • tree shaking도 활용 가능: 예를 들어, 유저 테이블에서는 problem filter, lang filter, level filter를 사용하지 않기 때문에 요청되는 JS 번들에서 해당 코드들은 포함하지 않게 됨.

사용 예시

페이지네이션
<DataTableRoot data={users} columns={columns}>
  <DataTable />
  <DataTablePagination />
</DataTableRoot>
검색인풋, 필터들 + 문제 임포트 버튼
 <DataTableRoot
    data={problems}
    columns={columns}
    selectedRowIds={selectedProblemIds}
    defaultPageSize={DEFAULT_PAGE_SIZE}
    defaultSortState={[{ id: 'select', desc: true }]}
  >
  <div className="flex gap-4">
    <DataTableSearchBar columndId="title" />
    <DataTableLangFilter />
    <DataTableLevelFilter />
    <ImportProblemButton onSelectedExport={onSelectedExport} />
  </div>
  <DataTable onRowClick={(table, row) => { /*...*/ }} />
  <DataTablePagination enableSelection showRowsPerPage={false} />
</DataTableRoot>
function ImportProblemButton({ onSelectedExport }: ImportProblemButtonProps) {
  const { table } = useDataTable<DataTableProblem>() // Context API를 통해 `table`에 접근

  const handleImportProblems = () => {
    const selectedRows = table
      .getSelectedRowModel()
      .rows.map((row) => row.original)

    const problems = selectedRows.map((problem, index) => ({
      id: problem.id,
      title: problem.title,
      difficulty: problem.difficulty,
      order: index,
      score: 0 // Score 기능 완료되면 수정해주세요!!
    }))

    onSelectedExport(problems)
  }

  return (
    <Button onClick={handleImportProblems} className="ml-auto">
      Import / Edit
    </Button>
  )
}
삭제 버튼
<DataTableRoot
  data={problems}
  columns={columns}
  defaultSortState={[{ id: 'updateTime', desc: true }]}
>
  <div className="flex gap-4">
    <DataTableSearchBar columndId="title" />
    <DataTableLangFilter />
    <DataTableLevelFilter />
    <ProblemsDeleteButton />
  </div>
  <DataTable getHref={(data) => `/admin/problem/${data.id}`} />
  <DataTablePagination enableSelection />
</DataTableRoot>

function ProblemsDeleteButton() {
  const client = useApolloClient()
  const [deleteProblem] = useMutation(DELETE_PROBLEM)
  const [fetchContests] = useLazyQuery(GET_BELONGED_CONTESTS)

  const getCanDelete = async (data: DataTableProblem[]) => {
    const promises = data.map((item) =>
      fetchContests({
        variables: {
          problemId: Number(item.id)
        }
      })
    )
    const results = await Promise.all(promises)
    const isAllSafe = results.every(({ data }) => data === undefined)

    if (isAllSafe) {
      return true
    }

    toast.error('Failed: Problem included in the contest')
    return false
  }

  const deleteTarget = (id: number) => {
    return deleteProblem({
      variables: {
        groupId: 1,
        id
      }
    })
  }

  const onSuccess = () => {
    client.refetchQueries({
      include: [GET_PROBLEMS]
    })
  }

  return (
    <DataTableDeleteButton
      target="problem"
      getCanDelete={getCanDelete}
      deleteTarget={deleteTarget}
      onSuccess={onSuccess}
      className="ml-auto"
    />
  )
}

이 PR 승인받으면, DataTableAdmin을 새로운 컴포넌트들로 하나씩 교체하고, 마지막에 DataTableAdmin 등 필요없는 컴포넌트들 삭제할 예정입니다.

Additional context


Before submitting the PR, please make sure you do the following

  • [x] Read the Contributing Guidelines
  • [x] Read the Contributing Guidelines and follow the Commit Convention
  • [x] Provide a description in this PR that addresses what the PR is solving, or reference the issue that it solves (e.g. fixes #123).
  • [ ] Ideally, include relevant tests that fail without this PR but pass with it.

eunnbi avatar Oct 02 '24 12:10 eunnbi