Skip to content

refactor(FE-347): Tanstack Table 도입 - #7

Merged
ilcm96 merged 2 commits into
mainfrom
refactor/FE-347
Feb 24, 2026
Merged

refactor(FE-347): Tanstack Table 도입#7
ilcm96 merged 2 commits into
mainfrom
refactor/FE-347

Conversation

@ilcm96

@ilcm96 ilcm96 commented Feb 24, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 관리자 페이지의 모든 데이터 테이블에 페이지네이션 및 정렬 기능 추가
    • 쿠폰, 활동, 회원, 결제 등 관리 페이지의 목록 조회 성능 개선
  • Refactor

    • 데이터 테이블 컴포넌트 통합으로 관리자 UI 일관성 강화
    • 검색 기능 개선으로 사용자 경험 향상

@ilcm96
ilcm96 requested a review from Copilot February 24, 2026 10:49
@ilcm96 ilcm96 self-assigned this Feb 24, 2026
@coderabbitai

coderabbitai Bot commented Feb 24, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@ilcm96 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 24 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 39c7648 and 3485368.

📒 Files selected for processing (1)
  • src/page/event/sections/EventToolbarSection.tsx

통합 개요

TanStack React-Table 라이브러리를 추가하고 새로운 AdminDataTable 컴포넌트를 도입하여 여러 관리자 페이지에서 페이지네이션, 정렬, 검색 기능을 일관되게 지원하도록 리팩토링했습니다. API 계층을 업데이트하여 구조화된 페이지 응답을 반환하고, 상태 관리 훅을 확장하여 페이지네이션 및 정렬 상태를 명시적으로 처리합니다.

변경 사항

Cohort / File(s) Summary
Package 의존성
package.json
TanStack React-Table 라이브러리 v8.21.3 추가
API 계층 - Activity
src/api/activity/get-activities.ts, src/api/activity/types.ts
ActivityPageQuery 인터페이스 추가, GetActivities 함수를 페이지네이션 쿼리 파라미터 지원으로 변경, AdminActivityPageResponse 응답 타입 추가
API 계층 - Coupon
src/api/coupon/get-coupons.ts, src/api/coupon/get-coupon-codes.ts, src/api/coupon/get-issued-coupons.ts, src/api/coupon/types.ts
CouponPageQuery, CouponCodePageQuery, IssuedCouponPageQuery 인터페이스 추가, API 함수들이 페이지네이션/정렬 파라미터를 지원하도록 수정, AdminCouponPageResponse, AdminCouponCodePageResponse, AdminIssuedCouponPageResponse 타입 추가
데이터 테이블 컴포넌트 라이브러리
src/components/admin/index.ts, src/components/admin/table/AdminDataTable.tsx, src/components/admin/table/sorting.ts, src/components/admin/table/types.ts
AdminDataTable 컴포넌트(재사용 가능한 제네릭 데이터 테이블, 정렬 및 페이지네이션 지원), 정렬 상태 관리 유틸리티 함수(createSortingState, normalizeSingleSorting, serializeSortingState), AdminDataTableProps 및 AdminColumnMeta 타입 정의
쿠폰 페이지 리팩토링
src/page/coupon.tsx, src/page/coupon/hooks/useCouponPageState.ts, src/page/coupon/sections/CouponPageHeader.tsx, src/page/coupon/sections/CouponTabSection.tsx, src/page/coupon/sections/CouponCodeTabSection.tsx, src/page/coupon/sections/CouponIssuedTabSection.tsx
AdminDataTable 기반 테이블 렌더링으로 전환, 페이지네이션/정렬 상태 명시화, 검색 드래프트/적용 패턴 도입, 각 탭 섹션에서 독립적인 페이지네이션/정렬 상태 관리
이벤트 페이지 리팩토링
src/page/event.tsx, src/page/event/hooks/useEventPageState.ts, src/page/event/sections/EventToolbarSection.tsx, src/page/event/sections/EventTableSection.tsx
AdminDataTable 적용, 검색 드래프트 및 적용 키워드 분리, 명시적 페이지네이션/정렬 상태 관리, 로딩 상태 전파
멤버 관리 페이지 리팩토링
src/page/member-management.tsx, src/page/member-management/hooks/useMemberManagementPageState.ts, src/page/member-management/sections/MemberRecordsSection.tsx
AdminDataTable로 수동 테이블 렌더링 대체, sortFilter를 sortPreset으로 변경, 체계적인 SortingState/PaginationState 도입, 정렬 프리셋과 정렬 상태 간 변환 유틸리티 추가
결제 페이지 리팩토링
src/page/payment/hooks/usePaymentPageState.ts, src/page/payment/sections/PaymentManagementSection.tsx, src/page/payment/sections/TransactionManagementSection.tsx
원시 page/sort 상태를 구조화된 PaginationState/SortingState로 전환, AdminDataTable 기반 테이블 렌더링, 정렬 맵 및 기본값 상수 추가, 강화된 에러 처리
포인트 페이지 리팩토링
src/page/point.tsx, src/page/point/hooks/usePointPageState.ts, src/page/point/sections/PointLedgerSection.tsx
ledgerPage를 ledgerPagination으로, ledgerSort를 ledgerSorting으로 변경, AdminDataTable 도입, SortingState/PaginationState 기반 상태 관리, 정렬/페이지네이션 변경 핸들러 추가

시퀀스 다이어그램

sequenceDiagram
    participant UI as UI 컴포넌트
    participant Hook as 상태 관리 훅
    participant API as API 계층
    participant Server as 서버
    
    UI->>Hook: onSortingChange / onPaginationChange 호출
    activate Hook
    Hook->>Hook: 정렬/페이지네이션 상태 업데이트
    Hook->>Hook: serializeSortingState로 정렬 상태를 문자열로 변환
    Hook->>API: fetchData(page, size, sort, keyword)
    deactivate Hook
    
    activate API
    API->>API: URL 쿼리 파라미터 구성 (page, size, sort, keyword)
    API->>Server: GET /admin/resource?page=0&size=50&sort=id,asc&keyword=...
    deactivate API
    
    activate Server
    Server->>Server: 페이지네이션/정렬 적용하여 데이터 조회
    Server-->>API: AdminPageResponse { content, page, size, totalElements, totalPages, hasNext }
    deactivate Server
    
    activate API
    API-->>Hook: 페이지 응답 반환
    deactivate API
    
    activate Hook
    Hook->>Hook: 페이지 데이터 및 페이지네이션 상태 업데이트
    Hook-->>UI: 업데이트된 상태 반환
    deactivate Hook
    
    activate UI
    UI->>UI: AdminDataTable에 columns, data, sorting, pagination 전달
    UI->>UI: 테이블 렌더링 (헤더, 바디, 페이지네이션 컨트롤)
    UI-->>UI: 사용자에게 표시
    deactivate UI
Loading
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 주요 변경 사항(TanStack Table 도입)을 명확하게 요약하고 있으며, 전체 변경 집합의 핵심 목적을 잘 나타내고 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (10)
src/api/coupon/get-coupon-codes.ts (1)

4-28: 쿼리 파라미터 빌드 로직 공통화 고려

getCouponCodes, getCoupons, getIssuedCoupons 등에서 page, size, sort, keyword 파라미터 빌드 로직이 동일하게 반복됩니다. 공통 유틸리티 함수로 추출하면 유지보수가 편해집니다.

♻️ 공통 유틸리티 예시
// src/api/common/query-params.ts
interface BasePageQuery {
  page?: number;
  size?: number;
  sort?: string;
  keyword?: string;
}

export function buildPageParams(query: BasePageQuery): URLSearchParams {
  const params = new URLSearchParams();
  params.set("page", String(query.page ?? 0));
  params.set("size", String(query.size ?? 50));
  if (query.sort) params.set("sort", query.sort);
  if (query.keyword?.trim()) params.set("keyword", query.keyword.trim());
  return params;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/coupon/get-coupon-codes.ts` around lines 4 - 28, Multiple API
functions (getCouponCodes, getCoupons, getIssuedCoupons) repeat identical
page/size/sort/keyword URLSearchParams building; extract that into a shared
utility (e.g., buildPageParams or buildPageQuery) that accepts a
BasePageQuery-like object and returns a URLSearchParams, then replace the inline
params construction in getCouponCodes with a call to that utility and use its
toString() when composing the request URL; ensure the utility applies the same
defaults (page -> 0, size -> 50) and trims/guards keyword and only sets
sort/keyword when present so existing behavior of
requestApi(`/admin/coupons/code?${params.toString()}`, { method: "GET" })
remains unchanged.
src/page/point/hooks/usePointPageState.ts (1)

155-165: void fetchLedger(...) 패턴과 eslint-disable 주석

마운트 시에만 fetch하려는 의도가 명확하며, void로 floating promise를 명시적으로 무시하고 있습니다. 다만 이 패턴이 여러 hook에서 반복될 경우, 커스텀 useMount 유틸로 추출하는 것도 고려해볼 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/page/point/hooks/usePointPageState.ts` around lines 155 - 165, The
useEffect currently calls a floating promise via void fetchLedger(...) and
disables eslint for deps; extract this mount-only behavior into a reusable hook
(e.g., create a useMount utility) and call fetchLedger from that hook instead of
the anonymous useEffect to avoid repeating the void pattern and eslint-disable.
Update usePointPageState to import and use useMount(() => {
fetchLedger(ledgerPagination, ledgerMemberKeyword, ledgerTransactionType,
ledgerSorting, ledgerFrom, ledgerTo) }) (or wrap fetchLedger call in an async
IIFE inside useMount) so the mount-only invocation is explicit and you can
remove the eslint-disable and the void usage. Ensure the referenced symbols are
fetchLedger and usePointPageState (and the new useMount helper) so reviewers can
locate changes.
src/api/coupon/types.ts (1)

43-68: 페이지 응답 타입에 제네릭 베이스 타입 도입 고려

AdminCouponPageResponse, AdminCouponCodePageResponse, AdminIssuedCouponPageResponse 모두 동일한 페이징 필드(page, size, totalElements, totalPages, hasNext)를 반복하고 있습니다. PR 전체적으로 이벤트, 멤버, 결제, 포인트 등에서도 동일한 패턴이 사용될 것으로 보이므로, 제네릭 베이스 타입을 정의하면 중복을 줄일 수 있습니다.

♻️ 제네릭 페이지 응답 타입 제안
+// 공통 페이지 응답 타입 (예: src/api/common/types.ts)
+export interface AdminPageResponse<T> {
+  content: T[]
+  page: number
+  size: number
+  totalElements: number
+  totalPages: number
+  hasNext: boolean
+}
+
-export interface AdminCouponPageResponse {
-  content: AdminCoupon[]
-  page: number
-  size: number
-  totalElements: number
-  totalPages: number
-  hasNext: boolean
-}
+export type AdminCouponPageResponse = AdminPageResponse<AdminCoupon>
-
-export interface AdminCouponCodePageResponse {
-  content: AdminCouponCode[]
-  page: number
-  size: number
-  totalElements: number
-  totalPages: number
-  hasNext: boolean
-}
+export type AdminCouponCodePageResponse = AdminPageResponse<AdminCouponCode>
-
-export interface AdminIssuedCouponPageResponse {
-  content: AdminIssuedCoupon[]
-  page: number
-  size: number
-  totalElements: number
-  totalPages: number
-  hasNext: boolean
-}
+export type AdminIssuedCouponPageResponse = AdminPageResponse<AdminIssuedCoupon>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/coupon/types.ts` around lines 43 - 68, Multiple page response
interfaces (AdminCouponPageResponse, AdminCouponCodePageResponse,
AdminIssuedCouponPageResponse) duplicate the same paging fields; introduce a
generic base like PageResponse<T> that defines content: T[], page, size,
totalElements, totalPages, hasNext, then replace each specific interface with
type aliases or extends (e.g., type AdminCouponPageResponse =
PageResponse<AdminCoupon>) using the existing domain types AdminCoupon,
AdminCouponCode, AdminIssuedCoupon; update any other similar page types (events,
members, payments, points) to use PageResponse<T> to remove duplication.
src/page/payment/hooks/usePaymentPageState.ts (1)

282-301: Pagination 변경 핸들러의 중복 로직을 유틸로 추출하면 좋겠습니다.

handlePaymentPaginationChangehandleTransactionPaginationChange는 boundary check 로직(pageIndex < 0, pageIndex >= totalPages)이 거의 동일하며, 이 패턴이 coupon 쪽에도 3번 반복됩니다. 공통 헬퍼 함수를 만들어 재사용하면 유지보수가 편해질 것입니다.

♻️ 헬퍼 예시
function createPaginationChangeHandler(
  currentPagination: PaginationState,
  totalPages: number | undefined,
  setPagination: (p: PaginationState) => void,
  onFetch: (p: PaginationState) => void,
) {
  return (updater: Updater<PaginationState>): void => {
    const next = functionalUpdate(updater, currentPagination)
    if (next.pageIndex < 0) return
    if (totalPages !== undefined && next.pageIndex >= totalPages) return
    setPagination(next)
    onFetch(next)
  }
}

Also applies to: 394-415

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/page/payment/hooks/usePaymentPageState.ts` around lines 282 - 301,
Extract the duplicated boundary-check + update + fetch logic into a reusable
helper (e.g., createPaginationChangeHandler) and replace
handlePaymentPaginationChange and handleTransactionPaginationChange (and the
three coupon handlers) to call that helper; the helper should accept
currentPagination (paymentPagination), totalPages (paymentData?.totalPages),
setPagination (setPaymentPagination) and onFetch (a wrapper for fetchPayments
with paymentYearSemester/paymentStatus/paymentSorting/paymentMemberKeyword
bound) and perform the functionalUpdate, pageIndex < 0 and pageIndex >=
totalPages checks, then call setPagination and onFetch with the next pagination.
src/components/admin/table/AdminDataTable.tsx (1)

73-76: text-right 문자열 포함 여부로 정렬 버튼 위치를 결정하는 것은 다소 취약합니다.

columnMeta?.headerClassName?.includes("text-right")에 의존하면, className 변경 시 정렬 버튼 정렬이 깨질 수 있습니다. AdminColumnMetaalign: "left" | "right" 같은 명시적 필드를 추가하는 것을 고려해 보세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/admin/table/AdminDataTable.tsx` around lines 73 - 76, The
current layout logic in AdminDataTable.tsx relies on checking
columnMeta?.headerClassName?.includes("text-right") to position the sort button,
which is brittle; update the AdminColumnMeta type to add an explicit align?:
"left" | "right" (or required align) field, update any places that construct
columnMeta to set align, and change the rendering logic in AdminDataTable (the
code around className={cn(...
columnMeta?.headerClassName?.includes("text-right") && "ml-auto flex", ...)}) to
use columnMeta.align === "right" instead of inspecting headerClassName; keep
headerClassName usage for styling but use the new align field for layout
decisions so changes to classes won’t affect alignment behavior.
src/page/coupon/hooks/useCouponPageState.ts (1)

437-501: 세 탭의 sorting/pagination 핸들러가 거의 동일합니다.

handleCouponSortingChange, handleCouponCodeSortingChange, handleIssuedSortingChange 그리고 pagination 핸들러 3개가 정렬 기본값과 상태 setter만 다르고 로직이 완전히 동일합니다. 헬퍼 팩토리 함수를 만들면 약 60줄을 줄일 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/page/coupon/hooks/useCouponPageState.ts` around lines 437 - 501, Many
handlers (handleCouponSortingChange, handleCouponCodeSortingChange,
handleIssuedSortingChange and their pagination counterparts) duplicate the same
logic; create a small factory that, given the relevant default sorting constant
(e.g. DEFAULT_COUPON_SORTING, DEFAULT_COUPON_CODE_SORTING,
DEFAULT_ISSUED_SORTING), the current pagination/page state
(couponPagination/couponPage, couponCodePagination/couponCodePage,
issuedPagination/issuedCouponPage) and the setters
(setCouponSorting/setCouponPagination,
setCouponCodeSorting/setCouponCodePagination,
setIssuedSorting/setIssuedPagination), returns the pair of handlers
(sortingChange, paginationChange) that call normalizeSingleSorting and
functionalUpdate, validate pageIndex bounds and reset pageIndex to 0 on sorting
change; replace the six inline functions with instances produced by this factory
so behavior remains identical while removing duplication.
src/page/member-management/hooks/useMemberManagementPageState.ts (1)

377-383: handleSortingChange에서 pagination 리셋이 useEffect와 안전하게 동작하는지 확인 필요.

setSortingsetRecordPagination이 같은 이벤트 핸들러 내에서 호출되므로 React 18+ batching 덕분에 단일 렌더에서 처리됩니다. 다만, 이 파일은 useEffect 기반 fetch 패턴을 사용하는 반면 usePaymentPageState는 핸들러 내에서 직접 fetch를 호출합니다. 동일 PR 내에서 두 가지 패턴이 혼재하는 점은 향후 혼란의 소지가 있습니다.

src/page/point/sections/PointLedgerSection.tsx (1)

78-78: transactionType 렌더링이 이진 분기에 의존합니다.

현재 "EARN" 외의 모든 값을 "차감"으로 표시합니다. 향후 새로운 트랜잭션 유형이 추가되면 의도치 않게 "차감"으로 표시될 수 있습니다. 명시적인 매핑이나 exhaustive check를 고려해 볼 수 있습니다.

♻️ 명시적 매핑 예시
-        cell: ({ row }) => (row.original.transactionType === "EARN" ? "적립" : "차감"),
+        cell: ({ row }) => {
+          const labels: Record<string, string> = { EARN: "적립", SPEND: "차감" }
+          return labels[row.original.transactionType] ?? row.original.transactionType
+        },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/page/point/sections/PointLedgerSection.tsx` at line 78, The current cell
renderer for row.original.transactionType uses a binary check
(row.original.transactionType === "EARN") and renders everything else as "차감";
change it to an explicit mapping or exhaustive switch so new transaction types
don’t silently render as "차감". Update the cell function (the column definition
using cell: ({ row }) => ...) to look up transactionType in a map (e.g., { EARN:
"적립", REDEEM: "차감", ... }) or use a switch with a clear default like "알 수 없음" or
the raw transactionType, ensuring all known types are listed and unknown types
get a non-misleading fallback.
src/page/coupon/sections/CouponCodeTabSection.tsx (1)

26-26: formatDateTime 유틸리티가 여러 파일에 중복 정의되어 있습니다.

src/page/coupon/hooks/useCouponPageState.tssrc/page/member-management/hooks/useMemberManagementPageState.ts에 동일한 구현의 formatDateTime이 존재합니다. 공용 유틸리티로 추출하면 DRY 원칙을 준수하고 일관성을 유지할 수 있습니다. 이 파일에 국한된 문제는 아니므로, 추후 별도 작업으로 진행해도 됩니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/page/coupon/sections/CouponCodeTabSection.tsx` at line 26, The duplicate
formatDateTime implementation appears in the hooks useCouponPageState and
useMemberManagementPageState; extract this helper into a shared utility (e.g.,
export function formatDateTime in a new utils/date.ts or utils/formatters.ts),
replace the local implementations by importing the shared formatDateTime, and
update both hooks to use the exported function so the logic is centralized and
DRY.
src/page/coupon/sections/CouponTabSection.tsx (1)

47-104: couponNameDraftsuseMemo 의존성에 포함되어 입력 시마다 컬럼이 재생성됩니다.

couponNameDrafts 객체는 사용자가 이름 수정 Input에 타이핑할 때마다 변경되므로, 매 키스트로크마다 전체 columns 배열이 재생성되고 TanStack Table이 컬럼을 다시 처리합니다. 어드민 페이지 규모에서는 문제가 없을 수 있지만, meta를 활용하여 couponNameDrafts를 테이블 메타로 전달하면 컬럼 정의를 안정적으로 유지할 수 있습니다.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/admin/table/AdminDataTable.tsx`:
- Around line 124-127: The current display uses a misleading fallback
"totalElements ?? rows.length" in AdminDataTable which shows only the current
page row count when totalElements is undefined; change this by either making
totalElements a required prop in the AdminDataTable props (update the props
type/interface and all callers to pass totalElements) or remove the fallback and
conditionally render the count only when totalElements is provided (e.g., show
nothing or a placeholder when totalElements is undefined). Update the span
rendering logic that references totalElements, and adjust any parent components
that instantiate AdminDataTable to supply the totalElements value if you choose
the required-prop approach.

In `@src/page/coupon/hooks/useCouponPageState.ts`:
- Around line 196-232: Both fetchCouponOptions and fetchMembers lack try/catch
blocks so an exception will reject the Promise.all call and prevent the other
result from applying; wrap the body of fetchCouponOptions and fetchMembers in
try/catch, handle errors individually by calling showError (for
fetchCouponOptions you can pass resolveCouponErrorMessage(error) or a fallback
message and ensure early return) and for fetchMembers show a user-friendly
error, and keep existing success paths that call setCoupons,
setCouponNameDrafts, and setMembers (references: getCoupons,
setCouponNameDrafts, setCoupons, getAdminMembers, setMembers) so each function
handles its own failures without propagating uncaught exceptions.
- Around line 234-275: The fetchCouponPage function currently lacks error
handling so if getCoupons throws the loading flag remains true; wrap the body of
fetchCouponPage in try/catch/finally, call setIsCouponLoading(true) before the
try, in catch call showError(resolveCouponErrorMessage(err?.message)) and
setCouponPage(null) (or similar error fallback), and in finally always call
setIsCouponLoading(false); apply the same try/catch/finally pattern to
fetchCouponCodePage and fetchIssuedCouponPage to mirror the error handling used
in usePaymentPageState and ensure the UI loading state is cleared on network or
thrown errors.

In `@src/page/event/sections/EventToolbarSection.tsx`:
- Around line 39-43: The onKeyDown handler currently calls onApplySearch on
Enter even during IME composition or when a search is already loading; update
the Enter branch in the onKeyDown handler to first check
event.nativeEvent.isComposing (or event.isComposing) and the component's
isLoading flag and return early if either is true, then only call onApplySearch
when not composing and not isLoading—refer to the onKeyDown handler and
onApplySearch symbols to locate where to add these guards.

In `@src/page/member-management/hooks/useMemberManagementPageState.ts`:
- Around line 385-397: handlePaginationChange blocks all navigation when
recordPage.totalPages is 0 because nextPagination.pageIndex >=
recordPage.totalPages is always true; change the guard in handlePaginationChange
so it only enforces the upper bound when totalPages > 0 (for example, require
recordPage.totalPages > 0 && nextPagination.pageIndex >= recordPage.totalPages),
ensuring you still keep the existing check for nextPagination.pageIndex < 0 and
then call setRecordPagination(nextPagination).

---

Nitpick comments:
In `@src/api/coupon/get-coupon-codes.ts`:
- Around line 4-28: Multiple API functions (getCouponCodes, getCoupons,
getIssuedCoupons) repeat identical page/size/sort/keyword URLSearchParams
building; extract that into a shared utility (e.g., buildPageParams or
buildPageQuery) that accepts a BasePageQuery-like object and returns a
URLSearchParams, then replace the inline params construction in getCouponCodes
with a call to that utility and use its toString() when composing the request
URL; ensure the utility applies the same defaults (page -> 0, size -> 50) and
trims/guards keyword and only sets sort/keyword when present so existing
behavior of requestApi(`/admin/coupons/code?${params.toString()}`, { method:
"GET" }) remains unchanged.

In `@src/api/coupon/types.ts`:
- Around line 43-68: Multiple page response interfaces (AdminCouponPageResponse,
AdminCouponCodePageResponse, AdminIssuedCouponPageResponse) duplicate the same
paging fields; introduce a generic base like PageResponse<T> that defines
content: T[], page, size, totalElements, totalPages, hasNext, then replace each
specific interface with type aliases or extends (e.g., type
AdminCouponPageResponse = PageResponse<AdminCoupon>) using the existing domain
types AdminCoupon, AdminCouponCode, AdminIssuedCoupon; update any other similar
page types (events, members, payments, points) to use PageResponse<T> to remove
duplication.

In `@src/components/admin/table/AdminDataTable.tsx`:
- Around line 73-76: The current layout logic in AdminDataTable.tsx relies on
checking columnMeta?.headerClassName?.includes("text-right") to position the
sort button, which is brittle; update the AdminColumnMeta type to add an
explicit align?: "left" | "right" (or required align) field, update any places
that construct columnMeta to set align, and change the rendering logic in
AdminDataTable (the code around className={cn(...
columnMeta?.headerClassName?.includes("text-right") && "ml-auto flex", ...)}) to
use columnMeta.align === "right" instead of inspecting headerClassName; keep
headerClassName usage for styling but use the new align field for layout
decisions so changes to classes won’t affect alignment behavior.

In `@src/page/coupon/hooks/useCouponPageState.ts`:
- Around line 437-501: Many handlers (handleCouponSortingChange,
handleCouponCodeSortingChange, handleIssuedSortingChange and their pagination
counterparts) duplicate the same logic; create a small factory that, given the
relevant default sorting constant (e.g. DEFAULT_COUPON_SORTING,
DEFAULT_COUPON_CODE_SORTING, DEFAULT_ISSUED_SORTING), the current
pagination/page state (couponPagination/couponPage,
couponCodePagination/couponCodePage, issuedPagination/issuedCouponPage) and the
setters (setCouponSorting/setCouponPagination,
setCouponCodeSorting/setCouponCodePagination,
setIssuedSorting/setIssuedPagination), returns the pair of handlers
(sortingChange, paginationChange) that call normalizeSingleSorting and
functionalUpdate, validate pageIndex bounds and reset pageIndex to 0 on sorting
change; replace the six inline functions with instances produced by this factory
so behavior remains identical while removing duplication.

In `@src/page/coupon/sections/CouponCodeTabSection.tsx`:
- Line 26: The duplicate formatDateTime implementation appears in the hooks
useCouponPageState and useMemberManagementPageState; extract this helper into a
shared utility (e.g., export function formatDateTime in a new utils/date.ts or
utils/formatters.ts), replace the local implementations by importing the shared
formatDateTime, and update both hooks to use the exported function so the logic
is centralized and DRY.

In `@src/page/payment/hooks/usePaymentPageState.ts`:
- Around line 282-301: Extract the duplicated boundary-check + update + fetch
logic into a reusable helper (e.g., createPaginationChangeHandler) and replace
handlePaymentPaginationChange and handleTransactionPaginationChange (and the
three coupon handlers) to call that helper; the helper should accept
currentPagination (paymentPagination), totalPages (paymentData?.totalPages),
setPagination (setPaymentPagination) and onFetch (a wrapper for fetchPayments
with paymentYearSemester/paymentStatus/paymentSorting/paymentMemberKeyword
bound) and perform the functionalUpdate, pageIndex < 0 and pageIndex >=
totalPages checks, then call setPagination and onFetch with the next pagination.

In `@src/page/point/hooks/usePointPageState.ts`:
- Around line 155-165: The useEffect currently calls a floating promise via void
fetchLedger(...) and disables eslint for deps; extract this mount-only behavior
into a reusable hook (e.g., create a useMount utility) and call fetchLedger from
that hook instead of the anonymous useEffect to avoid repeating the void pattern
and eslint-disable. Update usePointPageState to import and use useMount(() => {
fetchLedger(ledgerPagination, ledgerMemberKeyword, ledgerTransactionType,
ledgerSorting, ledgerFrom, ledgerTo) }) (or wrap fetchLedger call in an async
IIFE inside useMount) so the mount-only invocation is explicit and you can
remove the eslint-disable and the void usage. Ensure the referenced symbols are
fetchLedger and usePointPageState (and the new useMount helper) so reviewers can
locate changes.

In `@src/page/point/sections/PointLedgerSection.tsx`:
- Line 78: The current cell renderer for row.original.transactionType uses a
binary check (row.original.transactionType === "EARN") and renders everything
else as "차감"; change it to an explicit mapping or exhaustive switch so new
transaction types don’t silently render as "차감". Update the cell function (the
column definition using cell: ({ row }) => ...) to look up transactionType in a
map (e.g., { EARN: "적립", REDEEM: "차감", ... }) or use a switch with a clear
default like "알 수 없음" or the raw transactionType, ensuring all known types are
listed and unknown types get a non-misleading fallback.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 443a237 and 39c7648.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (30)
  • package.json
  • src/api/activity/get-activities.ts
  • src/api/activity/types.ts
  • src/api/coupon/get-coupon-codes.ts
  • src/api/coupon/get-coupons.ts
  • src/api/coupon/get-issued-coupons.ts
  • src/api/coupon/types.ts
  • src/components/admin/index.ts
  • src/components/admin/table/AdminDataTable.tsx
  • src/components/admin/table/sorting.ts
  • src/components/admin/table/types.ts
  • src/page/coupon.tsx
  • src/page/coupon/hooks/useCouponPageState.ts
  • src/page/coupon/sections/CouponCodeTabSection.tsx
  • src/page/coupon/sections/CouponIssuedTabSection.tsx
  • src/page/coupon/sections/CouponPageHeader.tsx
  • src/page/coupon/sections/CouponTabSection.tsx
  • src/page/event.tsx
  • src/page/event/hooks/useEventPageState.ts
  • src/page/event/sections/EventTableSection.tsx
  • src/page/event/sections/EventToolbarSection.tsx
  • src/page/member-management.tsx
  • src/page/member-management/hooks/useMemberManagementPageState.ts
  • src/page/member-management/sections/MemberRecordsSection.tsx
  • src/page/payment/hooks/usePaymentPageState.ts
  • src/page/payment/sections/PaymentManagementSection.tsx
  • src/page/payment/sections/TransactionManagementSection.tsx
  • src/page/point.tsx
  • src/page/point/hooks/usePointPageState.ts
  • src/page/point/sections/PointLedgerSection.tsx

Comment thread src/components/admin/table/AdminDataTable.tsx
Comment thread src/page/coupon/hooks/useCouponPageState.ts
Comment thread src/page/coupon/hooks/useCouponPageState.ts
Comment thread src/page/event/sections/EventToolbarSection.tsx
Comment thread src/page/member-management/hooks/useMemberManagementPageState.ts

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request refactors the admin interface to use TanStack Table (v8.21.3) library, replacing custom table implementations across all admin pages. The refactoring introduces a unified AdminDataTable component and standardizes pagination and sorting patterns throughout the application.

Changes:

  • Introduced AdminDataTable component with TanStack Table integration for consistent table behavior
  • Refactored all admin pages (Point, Payment, Member Management, Event, Coupon) to use the new table component
  • Updated API functions to support server-side pagination, sorting, and keyword search
  • Changed search behavior from instant filtering to button/Enter-triggered search with applied keyword display

Reviewed changes

Copilot reviewed 30 out of 31 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/components/admin/table/AdminDataTable.tsx New unified table component using TanStack Table with built-in pagination and sorting
src/components/admin/table/sorting.ts Utility functions for converting TanStack sorting state to backend API format
src/components/admin/table/types.ts Type definitions for AdminDataTable props and column metadata
src/components/admin/index.ts Added exports for new table components and utilities
src/page/point/sections/PointLedgerSection.tsx Migrated to AdminDataTable with column definitions using useMemo
src/page/point/hooks/usePointPageState.ts Refactored to use TanStack pagination/sorting state instead of custom implementation
src/page/point.tsx Updated props to pass sorting and pagination state
src/page/payment/sections/TransactionManagementSection.tsx Migrated to AdminDataTable pattern
src/page/payment/sections/PaymentManagementSection.tsx Migrated to AdminDataTable with action column support
src/page/payment/hooks/usePaymentPageState.ts Refactored state management for both payment and transaction tables
src/page/member-management/sections/MemberRecordsSection.tsx Migrated to AdminDataTable with row selection and custom styling
src/page/member-management/hooks/useMemberManagementPageState.ts Added sort preset conversion to sync dropdown with table sorting
src/page/member-management.tsx Updated props for new table implementation
src/page/event/sections/EventToolbarSection.tsx Added search button and applied keyword display
src/page/event/sections/EventTableSection.tsx Migrated to AdminDataTable with action buttons in columns
src/page/event/hooks/useEventPageState.ts Refactored to handle server-side pagination and keyword search
src/page/event.tsx Updated to pass pagination and sorting state
src/page/coupon/sections/CouponPageHeader.tsx Added search button and applied keyword display
src/page/coupon/sections/CouponTabSection.tsx Migrated coupon table to AdminDataTable
src/page/coupon/sections/CouponIssuedTabSection.tsx Migrated issued coupon table to AdminDataTable
src/page/coupon/sections/CouponCodeTabSection.tsx Migrated coupon code table to AdminDataTable
src/page/coupon/hooks/useCouponPageState.ts Refactored to manage separate pagination/sorting for three tabs
src/page/coupon.tsx Updated to remove loading/empty state cards (now handled by AdminDataTable)
src/api/coupon/types.ts Added paginated response types for coupons, codes, and issued coupons
src/api/coupon/get-issued-coupons.ts Changed to accept query parameters for pagination, sorting, and filtering
src/api/coupon/get-coupons.ts Changed to accept query parameters for pagination, sorting, and keyword search
src/api/coupon/get-coupon-codes.ts Changed to accept query parameters for pagination, sorting, and keyword search
src/api/activity/types.ts Added AdminActivityPageResponse type for paginated activities
src/api/activity/get-activities.ts Changed to accept query parameters for pagination, sorting, and keyword search
package.json Added @tanstack/react-table dependency (v8.21.3)
package-lock.json Lock file update for new dependency

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/components/admin/table/AdminDataTable.tsx
Comment thread src/page/event/hooks/useEventPageState.ts
Comment thread src/page/coupon/hooks/useCouponPageState.ts
Comment thread src/page/coupon/hooks/useCouponPageState.ts
@ilcm96
ilcm96 merged commit fd8cff7 into main Feb 24, 2026
2 checks passed
@ilcm96
ilcm96 deleted the refactor/FE-347 branch May 16, 2026 14:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants