diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-05-29 05:17:13 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-05-29 05:17:13 +0000 |
| commit | 37f55540833c2d5894513eca9fc8f7c6233fc2d2 (patch) | |
| tree | 6807978e7150358b3444c33b825c83e2c9cda8e8 /lib/items | |
| parent | 4b9bdb29e637f67761beb2db7f75dab0432d6712 (diff) | |
(대표님) 0529 14시 16분 변경사항 저장 (Vendor Data, Docu)
Diffstat (limited to 'lib/items')
| -rw-r--r-- | lib/items/service.ts | 201 | ||||
| -rw-r--r-- | lib/items/table/items-table.tsx | 161 | ||||
| -rw-r--r-- | lib/items/validations.ts | 6 |
3 files changed, 282 insertions, 86 deletions
diff --git a/lib/items/service.ts b/lib/items/service.ts index 99ef79ef..35d2fa01 100644 --- a/lib/items/service.ts +++ b/lib/items/service.ts @@ -25,29 +25,105 @@ import { countItems, deleteItemById, deleteItemsByIds, findAllItems, insertItem, * Next.js의 unstable_cache를 사용해 일정 시간 캐시. */ export async function getItems(input: GetItemsSchema) { - + const safePerPage = Math.min(input.perPage, 100); + return unstable_cache( async () => { try { - const offset = (input.page - 1) * input.perPage; + const offset = (input.page - 1) * safePerPage; + + const advancedWhere = filterColumns({ + table: items, + filters: input.filters, + joinOperator: input.joinOperator, + }); + + let globalWhere; + if (input.search) { + const s = `%${input.search}%`; + globalWhere = or( + ilike(items.itemLevel, s), + ilike(items.itemCode, s), + ilike(items.itemName, s), + ilike(items.description, s), + ilike(items.parentItemCode, s), + ilike(items.unitOfMeasure, s), + ilike(items.steelType, s), + ilike(items.gradeMaterial, s), + ilike(items.baseUnitOfMeasure, s), + ilike(items.changeDate, s) + ); + } + + const finalWhere = and(advancedWhere, globalWhere); + + const orderBy = input.sort.length > 0 + ? input.sort.map((item) => + item.desc ? desc(items[item.id]) : asc(items[item.id]) + ) + : [asc(items.createdAt)]; + + const { data, total } = await db.transaction(async (tx) => { + const data = await selectItems(tx, { + where: finalWhere, + orderBy, + offset, + limit: safePerPage, + }); + + const total = await countItems(tx, finalWhere); + return { data, total }; + }); + + const pageCount = Math.ceil(total / safePerPage); + return { data, pageCount }; + } catch (err) { + console.error(err); + return { data: [], pageCount: 0 }; + } + }, + [JSON.stringify({...input, perPage: safePerPage})], + { + revalidate: 3600, + tags: ["items"], + } + )(); +} + +export interface GetItemsInfiniteInput extends Omit<GetItemsSchema, 'page' | 'perPage'> { + cursor?: string; + limit?: number; +} + +// 무한 스크롤 결과 타입 +export interface GetItemsInfiniteResult { + data: any[]; + hasNextPage: boolean; + nextCursor: string | null; + total?: number | null; +} - // const advancedTable = input.flags.includes("advancedTable"); - const advancedTable = true; +export async function getItemsInfinite(input: GetItemsInfiniteInput): Promise<GetItemsInfiniteResult> { + return unstable_cache( + async () => { + try { + // 페이지 크기 제한 (기존과 동일한 방식) + const safeLimit = Math.min(input.limit || 50, 100); - // advancedTable 모드면 filterColumns()로 where 절 구성 + // 고급 필터링 (기존과 완전 동일) const advancedWhere = filterColumns({ table: items, filters: input.filters, joinOperator: input.joinOperator, }); - - let globalWhere + // 전역 검색 (기존과 완전 동일) + let globalWhere; if (input.search) { - const s = `%${input.search}%` + const s = `%${input.search}%`; globalWhere = or( ilike(items.itemLevel, s), - ilike(items.itemCode, s), + ilike(items.itemCode, s), ilike(items.itemName, s), ilike(items.description, s), ilike(items.parentItemCode, s), @@ -56,58 +132,107 @@ export async function getItems(input: GetItemsSchema) { ilike(items.gradeMaterial, s), ilike(items.baseUnitOfMeasure, s), ilike(items.changeDate, s) - ) - // 필요시 여러 칼럼 OR조건 (status, priority, etc) + ); } - const finalWhere = and( - // advancedWhere or your existing conditions - advancedWhere, - globalWhere // and()함수로 결합 or or() 등으로 결합 - ) - + // 커서 기반 페이지네이션 조건 추가 + let cursorWhere; + if (input.cursor) { + cursorWhere = gt(items.id, input.cursor); + } - // 아니면 ilike, inArray, gte 등으로 where 절 구성 - const where = finalWhere - + // 모든 조건 결합 + const finalWhere = and(advancedWhere, globalWhere, cursorWhere); + + // 정렬 (기존과 동일하지만 id 정렬 보장) + let orderBy = input.sort.length > 0 + ? input.sort.map((item) => + item.desc ? desc(items[item.id]) : asc(items[item.id]) + ) + : [asc(items.createdAt)]; + + // 무한 스크롤에서는 id 정렬이 필수 (커서 기반 페이지네이션용) + const hasIdSort = orderBy.some(sort => { + const column = sort.constructor.name.includes('desc') + ? sort.column + : sort.column; + return column === items.id; + }); - const orderBy = - input.sort.length > 0 - ? input.sort.map((item) => - item.desc ? desc(items[item.id]) : asc(items[item.id]) - ) - : [asc(items.createdAt)]; + if (!hasIdSort) { + orderBy.push(asc(items.id)); + } - // 트랜잭션 내부에서 Repository 호출 + // 트랜잭션으로 데이터 조회 (기존과 동일한 패턴) const { data, total } = await db.transaction(async (tx) => { + // limit + 1로 다음 페이지 존재 여부 확인 const data = await selectItems(tx, { - where, + where: finalWhere, orderBy, - offset, - limit: input.perPage, + limit: safeLimit + 1, }); - const total = await countItems(tx, where); + // 첫 페이지에서만 전체 개수 계산 (성능 최적화) + let total = null; + if (!input.cursor) { + // 커서 조건 제외하고 전체 개수 계산 + const countWhere = and(advancedWhere, globalWhere); + total = await countItems(tx, countWhere); + } + return { data, total }; }); - const pageCount = Math.ceil(total / input.perPage); + // 다음 페이지 존재 여부 및 커서 설정 + const hasNextPage = data.length > safeLimit; + const resultItems = hasNextPage ? data.slice(0, safeLimit) : data; + const nextCursor = hasNextPage && resultItems.length > 0 + ? resultItems[resultItems.length - 1].id + : null; + + return { + data: resultItems, + hasNextPage, + nextCursor, + total, + }; - return { data, pageCount }; } catch (err) { - // 에러 발생 시 디폴트 - console.error(err) - return { data: [], pageCount: 0 }; + console.error('getItemsInfinite error:', err); + return { + data: [], + hasNextPage: false, + nextCursor: null, + total: 0, + }; } }, - [JSON.stringify(input)], // 캐싱 키 + [JSON.stringify({ ...input, limit: Math.min(input.limit || 50, 100) })], { revalidate: 3600, - tags: ["items"], // revalidateTag("items") 호출 시 무효화 + tags: ["items"], } )(); } +// 통합된 Items 조회 함수 (모드별 자동 분기) +export async function getItemsUnified(input: GetItemsSchema & { mode?: 'pagination' | 'infinite'; cursor?: string }): Promise<any> { + // perPage 기반 모드 자동 결정 + const isInfiniteMode = input.perPage >= 1_000_000; + + if (isInfiniteMode || input.mode === 'infinite') { + // 무한 스크롤 모드 + return getItemsInfinite({ + ...input, + limit: 50, // 실제로는 50개씩 로드 + cursor: input.cursor, + }); + } else { + // 기존 페이지네이션 모드 + return getItems(input); + } +} + /* ----------------------------------------------------- 2) 생성(Create) diff --git a/lib/items/table/items-table.tsx b/lib/items/table/items-table.tsx index 2bc1c913..c05b4348 100644 --- a/lib/items/table/items-table.tsx +++ b/lib/items/table/items-table.tsx @@ -9,8 +9,12 @@ import type { import { useDataTable } from "@/hooks/use-data-table" import { DataTable } from "@/components/data-table/data-table" +import { InfiniteDataTable } from "@/components/data-table/infinite-data-table" import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" import { useFeatureFlags } from "./feature-flags-provider" +import { Button } from "@/components/ui/button" +import { RotateCcw, Info } from "lucide-react" +import { Alert, AlertDescription } from "@/components/ui/alert" import { getItems } from "../service" import { Item } from "@/db/schema/items" @@ -20,7 +24,7 @@ import { UpdateItemSheet } from "./update-item-sheet" import { DeleteItemsDialog } from "./delete-items-dialog" interface ItemsTableProps { - promises: Promise< + promises?: Promise< [ Awaited<ReturnType<typeof getItems>>, ] @@ -30,11 +34,11 @@ interface ItemsTableProps { export function ItemsTable({ promises }: ItemsTableProps) { const { featureFlags } = useFeatureFlags() - const [{ data, pageCount }] = - React.use(promises) - - console.log(data) + // 페이지네이션 모드 데이터 + const paginationData = promises ? React.use(promises) : null + const [{ data = [], pageCount = 0 }] = paginationData || [{ data: [], pageCount: 0 }] + console.log('ItemsTable data:', data.length, 'items') const [rowAction, setRowAction] = React.useState<DataTableRowAction<Item> | null>(null) @@ -44,17 +48,7 @@ export function ItemsTable({ promises }: ItemsTableProps) { [setRowAction] ) - /** - * This component can render either a faceted filter or a search filter based on the `options` prop. - * - * @prop options - An array of objects, each representing a filter option. If provided, a faceted filter is rendered. If not, a search filter is rendered. - * - * Each `option` object has the following properties: - * @prop {string} label - The label for the filter option. - * @prop {string} value - The value for the filter option. - * @prop {React.ReactNode} [icon] - An optional icon to display next to the label. - * @prop {boolean} [withCount] - An optional boolean to display the count of the filter option. - */ + // 기존 필터 필드들 const filterFields: DataTableFilterField<Item>[] = [ { id: "itemCode", @@ -62,16 +56,6 @@ export function ItemsTable({ promises }: ItemsTableProps) { }, ] - /** - * Advanced filter fields for the data table. - * These fields provide more complex filtering options compared to the regular filterFields. - * - * Key differences from regular filterFields: - * 1. More field types: Includes 'text', 'multi-select', 'date', and 'boolean'. - * 2. Enhanced flexibility: Allows for more precise and varied filtering options. - * 3. Used with DataTableAdvancedToolbar: Enables a more sophisticated filtering UI. - * 4. Date and boolean types: Adds support for filtering by date ranges and boolean values. - */ const advancedFilterFields: DataTableAdvancedFilterField<Item>[] = [ { id: "itemLevel", @@ -130,8 +114,15 @@ export function ItemsTable({ promises }: ItemsTableProps) { }, ] - - const { table } = useDataTable({ + // 확장된 useDataTable 훅 사용 (pageSize 기반 자동 전환) + const { + table, + infiniteScroll, + isInfiniteMode, + effectivePageSize, + handlePageSizeChange, + urlState + } = useDataTable({ data, columns, pageCount, @@ -145,24 +136,104 @@ export function ItemsTable({ promises }: ItemsTableProps) { getRowId: (originalRow) => String(originalRow.id), shallow: false, clearOnDefault: true, + // 무한 스크롤 설정 + infiniteScrollConfig: { + apiEndpoint: "/api/table/items/infinite", + tableName: "items", + maxPageSize: 100, + }, }) - return ( - <> - <DataTable - table={table} - - > - - <DataTableAdvancedToolbar - table={table} - filterFields={advancedFilterFields} - shallow={false} - > - <ItemsTableToolbarActions table={table} /> - </DataTableAdvancedToolbar> + // 새로고침 핸들러 + const handleRefresh = () => { + if (isInfiniteMode && infiniteScroll) { + infiniteScroll.refresh() + } else { + window.location.reload() + } + } - </DataTable> + return ( + <div className="w-full space-y-2.5"> + + {/* <div className="flex items-center justify-between"> + <div className="flex items-center gap-2"> + <Button + variant="outline" + size="sm" + onClick={handleRefresh} + disabled={isInfiniteMode && infiniteScroll?.isLoading} + > + <RotateCcw className="h-4 w-4 mr-2" /> + 새로고침 + </Button> + </div> + </div> */} + + {/* 에러 상태 (무한 스크롤 모드) */} + {isInfiniteMode && infiniteScroll?.error && ( + <Alert variant="destructive"> + <AlertDescription> + 데이터를 불러오는 중 오류가 발생했습니다. + <Button + variant="link" + size="sm" + onClick={() => infiniteScroll.reset()} + className="ml-2 p-0 h-auto" + > + 다시 시도 + </Button> + </AlertDescription> + </Alert> + )} + + {/* 로딩 상태가 아닐 때만 테이블 렌더링 */} + {!(isInfiniteMode && infiniteScroll?.isLoading && infiniteScroll?.isEmpty) ? ( + <> + {/* 도구 모음 */} + <DataTableAdvancedToolbar + table={table} + filterFields={advancedFilterFields} + shallow={false} + > + <ItemsTableToolbarActions table={table} /> + </DataTableAdvancedToolbar> + + {/* 테이블 렌더링 */} + {isInfiniteMode ? ( + // 무한 스크롤 모드: InfiniteDataTable 사용 (자체 페이지네이션 없음) + <InfiniteDataTable + table={table} + hasNextPage={infiniteScroll?.hasNextPage || false} + isLoadingMore={infiniteScroll?.isLoadingMore || false} + onLoadMore={infiniteScroll?.loadMore} + totalCount={infiniteScroll?.totalCount} + isEmpty={infiniteScroll?.isEmpty || false} + compact={false} + autoSizeColumns={true} + /> + ) : ( + // 페이지네이션 모드: DataTable 사용 (내장 페이지네이션 활용) + <DataTable + table={table} + compact={false} + autoSizeColumns={true} + /> + )} + </> + ) : ( + /* 로딩 스켈레톤 (무한 스크롤 초기 로딩) */ + <div className="space-y-3"> + <div className="text-sm text-muted-foreground mb-4"> + 무한 스크롤 모드로 데이터를 로드하고 있습니다... + </div> + {Array.from({ length: 10 }).map((_, i) => ( + <div key={i} className="h-12 w-full bg-muted animate-pulse rounded" /> + ))} + </div> + )} + + {/* 기존 다이얼로그들 */} <UpdateItemSheet open={rowAction?.type === "update"} onOpenChange={() => setRowAction(null)} @@ -175,6 +246,6 @@ export function ItemsTable({ promises }: ItemsTableProps) { showTrigger={false} onSuccess={() => rowAction?.row.toggleSelected(false)} /> - </> + </div> ) -} +}
\ No newline at end of file diff --git a/lib/items/validations.ts b/lib/items/validations.ts index 14fc27b1..bb90e931 100644 --- a/lib/items/validations.ts +++ b/lib/items/validations.ts @@ -37,6 +37,7 @@ export const searchParamsCache = createSearchParamsCache({ search: parseAsString.withDefault(""), }) +export type GetItemsSchema = Awaited<ReturnType<typeof searchParamsCache.parse>> export const createItemSchema = z.object({ itemCode: z.string().min(1, "아이템 코드는 필수입니다"), @@ -51,6 +52,8 @@ export const createItemSchema = z.object({ changeDate: z.string().max(8).nullable().optional(), baseUnitOfMeasure: z.string().max(3).nullable().optional(), }) +export type CreateItemSchema = z.infer<typeof createItemSchema> + export const updateItemSchema = z.object({ itemCode: z.string().optional(), @@ -65,7 +68,4 @@ export const updateItemSchema = z.object({ changeDate: z.string().max(8).nullable().optional(), baseUnitOfMeasure: z.string().max(3).nullable().optional(), }) - -export type GetItemsSchema = Awaited<ReturnType<typeof searchParamsCache.parse>> -export type CreateItemSchema = z.infer<typeof createItemSchema> export type UpdateItemSchema = z.infer<typeof updateItemSchema> |
