summaryrefslogtreecommitdiff
path: root/lib/items
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-05-29 05:17:13 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-05-29 05:17:13 +0000
commit37f55540833c2d5894513eca9fc8f7c6233fc2d2 (patch)
tree6807978e7150358b3444c33b825c83e2c9cda8e8 /lib/items
parent4b9bdb29e637f67761beb2db7f75dab0432d6712 (diff)
(대표님) 0529 14시 16분 변경사항 저장 (Vendor Data, Docu)
Diffstat (limited to 'lib/items')
-rw-r--r--lib/items/service.ts201
-rw-r--r--lib/items/table/items-table.tsx161
-rw-r--r--lib/items/validations.ts6
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>