codedang
codedang copied to clipboard
feat(fe): poc - new data table for admin
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.