summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-09-04 02:49:48 +0000
committerjoonhoekim <26rote@gmail.com>2025-09-04 02:49:48 +0000
commitc2672935caf9ce977840657cbff0da8af8e12f97 (patch)
tree6b50dd0c57bbd76eca7a938d623b012490459b9c
parent39f21a1e0f7491e9e70a2e41f07d691d179b3126 (diff)
(김준회) 기준정보-자재그룹 메뉴 구현
- 기준정보 메뉴를 자재그룹코드(/material-groups)로 변경 - 기존 자재코드조회는 /materials 로 남겨둠 - 메뉴를 자재그룹코드로만 남기고, i18n 처리
-rw-r--r--app/[lng]/evcp/(evcp)/material-groups/page.tsx76
-rw-r--r--app/api/table/material-groups/infinite/route.ts93
-rw-r--r--config/menuConfig.ts4
-rw-r--r--db/schema/items.ts2
-rw-r--r--i18n/locales/en/menu.json4
-rw-r--r--i18n/locales/ko/menu.json4
-rw-r--r--lib/material-groups/services.ts165
-rw-r--r--lib/material-groups/table/material-group-table-columns.tsx61
-rw-r--r--lib/material-groups/table/material-group-table.tsx178
-rw-r--r--lib/material-groups/validations.ts31
10 files changed, 611 insertions, 7 deletions
diff --git a/app/[lng]/evcp/(evcp)/material-groups/page.tsx b/app/[lng]/evcp/(evcp)/material-groups/page.tsx
new file mode 100644
index 00000000..3acd11b9
--- /dev/null
+++ b/app/[lng]/evcp/(evcp)/material-groups/page.tsx
@@ -0,0 +1,76 @@
+/**
+ * 자재그룹 테이블
+ * materialSearchView를 사용하여 MDG 자재마스터의 고유한 자재그룹 조회
+ * 수정/추가 기능은 불필요 (읽기 전용)
+ */
+
+import * as React from "react"
+import { type SearchParams } from "@/types/table"
+
+import { Skeleton } from "@/components/ui/skeleton"
+import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
+import { Shell } from "@/components/shell"
+import { getMaterialGroups } from "@/lib/material-groups/services"
+import { MaterialGroupTable } from "@/lib/material-groups/table/material-group-table"
+import { InformationButton } from "@/components/information/information-button"
+import { searchParamsCache } from "@/lib/material-groups/validations"
+
+interface MaterialGroupPageProps {
+ searchParams: Promise<SearchParams>
+}
+
+export default async function MaterialGroupPage(props: MaterialGroupPageProps) {
+ const searchParams = await props.searchParams
+
+ // searchParamsCache를 사용해서 파라미터 파싱
+ const search = searchParamsCache.parse(searchParams)
+
+ // pageSize 기반으로 모드 자동 결정
+ const isInfiniteMode = search.perPage >= 1_000_000
+
+ // 페이지네이션 모드일 때만 서버에서 데이터 가져오기
+ // 무한 스크롤 모드에서는 클라이언트에서 SWR로 데이터 로드
+ const promises = isInfiniteMode
+ ? undefined
+ : Promise.all([
+ getMaterialGroups(search),
+ ])
+
+ return (
+ <Shell className="gap-2">
+ <div className="flex items-center justify-between space-y-2">
+ <div className="flex items-center justify-between space-y-2">
+ <div>
+ <div className="flex items-center gap-2">
+ <h2 className="text-2xl font-bold tracking-tight">
+ 자재그룹
+ </h2>
+ <InformationButton pagePath="evcp/material-groups" />
+ </div>
+ <p className="text-muted-foreground">
+ MDG로부터 수신된 자재그룹 정보
+ </p>
+ </div>
+ </div>
+ </div>
+
+ <React.Suspense fallback={<Skeleton className="h-7 w-52" />}>
+ {/* 추가 컴포넌트가 필요한 경우 여기에 */}
+ </React.Suspense>
+
+ <React.Suspense
+ fallback={
+ <DataTableSkeleton
+ columnCount={2}
+ searchableColumnCount={1}
+ filterableColumnCount={2}
+ cellWidths={["15rem", "25rem"]}
+ shrinkZero
+ />
+ }
+ >
+ <MaterialGroupTable promises={promises} />
+ </React.Suspense>
+ </Shell>
+ )
+}
diff --git a/app/api/table/material-groups/infinite/route.ts b/app/api/table/material-groups/infinite/route.ts
new file mode 100644
index 00000000..1201ab77
--- /dev/null
+++ b/app/api/table/material-groups/infinite/route.ts
@@ -0,0 +1,93 @@
+// app/api/table/material-groups/infinite/route.ts
+import { NextRequest, NextResponse } from "next/server";
+import { getMaterialGroupsInfinite, type GetMaterialGroupsInfiniteInput } from "@/lib/material-groups/services";
+
+// URL 파라미터를 GetMaterialGroupsInfiniteInput으로 변환하는 헬퍼 함수
+function parseUrlParamsToInfiniteInput(searchParams: URLSearchParams): GetMaterialGroupsInfiniteInput {
+
+ const cursor = searchParams.get("cursor") || undefined;
+ const limit = parseInt(searchParams.get("limit") || "50");
+
+ // 고급 필터링 관련
+ const search = searchParams.get("search") || "";
+ const joinOperator = searchParams.get("joinOperator") || "and";
+
+ // 필터 파라미터 파싱
+ let filters: any[] = [];
+ const filtersParam = searchParams.get("filters");
+ if (filtersParam) {
+ try {
+ filters = JSON.parse(filtersParam);
+ } catch (e) {
+ console.warn("Invalid filters parameter:", e);
+ filters = [];
+ }
+ }
+
+ // 정렬 파라미터 파싱
+ let sort: Array<{ id: string; desc: boolean }> = [{ id: "materialGroupCode", desc: false }];
+ const sortParam = searchParams.get("sort");
+ if (sortParam) {
+ try {
+ sort = JSON.parse(sortParam);
+ } catch (e) {
+ console.warn("Invalid sort parameter:", e);
+ // 기본 정렬
+ sort = [{ id: "materialGroupCode", desc: false }];
+ }
+ } else {
+ // 정렬이 없으면 기본 정렬
+ sort = [{ id: "materialGroupCode", desc: false }];
+ }
+
+ return {
+ // 무한 스크롤 관련
+ limit,
+
+ // 고급 필터링
+ search,
+ filters,
+ joinOperator: joinOperator as "and" | "or",
+
+ // 정렬
+ sort,
+ };
+}
+
+export async function GET(request: NextRequest) {
+ try {
+ const { searchParams } = new URL(request.url);
+ console.log("=== Material Groups Infinite API ===");
+ console.log("Raw searchParams:", Object.fromEntries(searchParams.entries()));
+
+ // URL 파라미터 파싱
+ const input = parseUrlParamsToInfiniteInput(searchParams);
+ console.log("Parsed input:", input);
+
+ // 데이터 조회
+ const result = await getMaterialGroupsInfinite(input);
+ console.log("Query result count:", result.data.length);
+
+ // 응답 구성
+ const response = {
+ items: result.data,
+ hasNextPage: false, // 무한 스크롤에서는 모든 데이터를 한번에 로드
+ nextCursor: null,
+ total: result.data.length,
+ };
+
+ return NextResponse.json(response);
+ } catch (error) {
+ console.error("Material Groups infinite API error:", error);
+ return NextResponse.json(
+ {
+ error: "Internal server error",
+ items: [],
+ hasNextPage: false,
+ nextCursor: null,
+ total: 0,
+ },
+ { status: 500 }
+ );
+ }
+}
diff --git a/config/menuConfig.ts b/config/menuConfig.ts
index 955b24c4..605941a5 100644
--- a/config/menuConfig.ts
+++ b/config/menuConfig.ts
@@ -50,7 +50,7 @@ export const mainNav: MenuSection[] = [
},
{
titleKey: "menu.master_data.material_master",
- href: "/evcp/materials",
+ href: "/evcp/material-groups",
descriptionKey: "menu.master_data.material_master_desc",
groupKey: "groups.basic_info"
},
@@ -471,7 +471,7 @@ export const procurementNav: MenuSection[] = [
},
{
titleKey: "menu.master_data.material_master",
- href: "/evcp/materials",
+ href: "/evcp/material-groups",
descriptionKey: "menu.master_data.material_master_desc",
groupKey: "groups.basic_info"
},
diff --git a/db/schema/items.ts b/db/schema/items.ts
index 16338671..a7eb2a3f 100644
--- a/db/schema/items.ts
+++ b/db/schema/items.ts
@@ -89,7 +89,7 @@ export type ItemOffshoreHull = typeof itemOffshoreHull.$inferSelect;
//각 테이블별 컬럼 변경(itemid -> itemCode)
-// 자재 검색용 뷰 - MDG 스키마의 MATERIAL_MASTER_PART_MATL 테이블에서 DISTINCT 조회
+// 자재그룹 검색용 뷰 - MDG 스키마의 MATERIAL_MASTER_PART_MATL 테이블에서 DISTINCT 조회
export const materialSearchView = pgView("material_search_view").as((qb) => {
return qb
.select({
diff --git a/i18n/locales/en/menu.json b/i18n/locales/en/menu.json
index 521bafd0..33ade381 100644
--- a/i18n/locales/en/menu.json
+++ b/i18n/locales/en/menu.json
@@ -38,8 +38,8 @@
"projects": "Project List",
"projects_desc": "Project list received from MDG (C)",
"package_numbers": "Package Numbers",
- "material_master": "Material Master",
- "material_master_desc": "Material master list received from MDG",
+ "material_master": "Material Groups",
+ "material_master_desc": "Material groups list received from MDG",
"package_numbers_desc": "Package number list used in quotation (before PR issuance), bidding (before PR issuance), design data and documents",
"object_class": "Object Class List",
"object_class_desc": "Object class list",
diff --git a/i18n/locales/ko/menu.json b/i18n/locales/ko/menu.json
index e41d5727..bb8e4c00 100644
--- a/i18n/locales/ko/menu.json
+++ b/i18n/locales/ko/menu.json
@@ -37,8 +37,8 @@
"projects": "프로젝트 리스트",
"projects_desc": "MDG에서 받은 프로젝트 리스트(C)",
"package_numbers": "패키지 넘버",
- "material_master": "자재 마스터",
- "material_master_desc": "MDG에서 받은 자재 마스터 목록",
+ "material_master": "자재 그룹",
+ "material_master_desc": "MDG에서 받은 자재 그룹 목록",
"package_numbers_desc": "견적(PR 발행 전), 입찰(PR 발행 전), 설계 데이터 및 문서에서 사용되는 패키지 넘버 목록",
"object_class": "객체 클래스 목록",
"object_class_desc": "객체 클래스 목록",
diff --git a/lib/material-groups/services.ts b/lib/material-groups/services.ts
new file mode 100644
index 00000000..4b7e9dea
--- /dev/null
+++ b/lib/material-groups/services.ts
@@ -0,0 +1,165 @@
+'use server'
+
+import { and, asc, desc, ilike, or, sql, eq } from 'drizzle-orm';
+import db from '@/db/db';
+import { filterColumns } from "@/lib/filter-columns";
+import { materialSearchView } from "@/db/schema/items";
+
+// 자재그룹 뷰의 컬럼 타입 정의
+type MaterialGroupColumn = keyof typeof materialSearchView.$inferSelect;
+
+export interface GetMaterialGroupsInput {
+ page: number;
+ perPage: number;
+ search?: string;
+ sort: Array<{
+ id: MaterialGroupColumn;
+ desc: boolean;
+ }>;
+ filters?: any[];
+ joinOperator: 'and' | 'or';
+}
+
+/**
+ * 자재그룹 목록을 조회합니다.
+ * materialSearchView를 사용하여 MATKL(자재그룹코드)와 ZZNAME(자재그룹명)의 고유한 조합을 조회
+ */
+export async function getMaterialGroups(input: GetMaterialGroupsInput) {
+ const safePerPage = Math.min(input.perPage, 100);
+
+ try {
+ const offset = (input.page - 1) * safePerPage;
+
+ // 고급 필터링
+ const advancedWhere = filterColumns({
+ table: materialSearchView,
+ filters: (input.filters || []) as any,
+ joinOperator: input.joinOperator,
+ });
+
+ // 전역 검색 - 주요 컬럼들에 대해 검색
+ let globalWhere;
+ if (input.search) {
+ const s = `%${input.search}%`;
+ globalWhere = or(
+ ilike(materialSearchView.materialGroupCode, s), // 자재그룹코드
+ ilike(materialSearchView.materialName, s), // 자재그룹명
+ ilike(materialSearchView.displayText, s), // 표시 텍스트
+ );
+ }
+
+ const finalWhere = and(advancedWhere, globalWhere);
+
+ // 정렬 처리 - 타입 안전하게 처리
+ const orderBy = input.sort.length > 0
+ ? input.sort.map((item) => {
+ const column = materialSearchView[item.id];
+ return item.desc ? desc(column) : asc(column);
+ })
+ : [asc(materialSearchView.materialGroupCode)];
+
+ // 데이터 조회
+ const { data, total } = await db.transaction(async (tx) => {
+ const data = await tx
+ .select({
+ materialGroupCode: materialSearchView.materialGroupCode,
+ materialName: materialSearchView.materialName,
+ displayText: materialSearchView.displayText,
+ })
+ .from(materialSearchView)
+ .where(finalWhere)
+ .orderBy(...orderBy)
+ .offset(offset)
+ .limit(safePerPage);
+
+ const totalResult = await tx
+ .select({
+ count: sql<number>`count(*)`
+ })
+ .from(materialSearchView)
+ .where(finalWhere);
+
+ const total = Number(totalResult[0]?.count) || 0;
+ return { data, total };
+ });
+
+ const pageCount = Math.ceil(total / safePerPage);
+ return { data, pageCount };
+ } catch (err) {
+ console.error('Error in getMaterialGroups:', err);
+ return { data: [], pageCount: 0 };
+ }
+}
+
+/**
+ * 무한 스크롤을 위한 자재그룹 조회 (페이지네이션 없음)
+ */
+export interface GetMaterialGroupsInfiniteInput extends Omit<GetMaterialGroupsInput, 'page' | 'perPage'> {
+ limit?: number; // 무한 스크롤용 추가 옵션
+}
+
+export async function getMaterialGroupsInfinite(input: GetMaterialGroupsInfiniteInput) {
+ try {
+ // 고급 필터링
+ const advancedWhere = filterColumns({
+ table: materialSearchView,
+ filters: (input.filters || []) as any,
+ joinOperator: input.joinOperator || "and",
+ });
+
+ // 전역 검색
+ let globalWhere;
+ if (input.search) {
+ const s = `%${input.search}%`;
+ globalWhere = or(
+ ilike(materialSearchView.materialGroupCode, s),
+ ilike(materialSearchView.materialName, s),
+ ilike(materialSearchView.displayText, s),
+ );
+ }
+
+ const finalWhere = and(advancedWhere, globalWhere);
+
+ // 정렬 처리 - 타입 안전하게 처리
+ const orderBy = input.sort.length > 0
+ ? input.sort.map((item) => {
+ const column = materialSearchView[item.id];
+ return item.desc ? desc(column) : asc(column);
+ })
+ : [asc(materialSearchView.materialGroupCode)];
+
+ // 전체 데이터 조회 (클라이언트에서 가상화 처리)
+ const data = await db
+ .select({
+ materialGroupCode: materialSearchView.materialGroupCode,
+ materialName: materialSearchView.materialName,
+ displayText: materialSearchView.displayText,
+ })
+ .from(materialSearchView)
+ .where(finalWhere)
+ .orderBy(...orderBy);
+
+ return { data };
+ } catch (err) {
+ console.error('Error in getMaterialGroupsInfinite:', err);
+ return { data: [] };
+ }
+}
+
+/**
+ * 특정 자재그룹 상세 정보 조회
+ */
+export async function getMaterialGroupDetail(materialGroupCode: string) {
+ try {
+ const materialGroup = await db
+ .select()
+ .from(materialSearchView)
+ .where(eq(materialSearchView.materialGroupCode, materialGroupCode))
+ .limit(1);
+
+ return materialGroup.length > 0 ? materialGroup[0] : null;
+ } catch (err) {
+ console.error('Error in getMaterialGroupDetail:', err);
+ return null;
+ }
+}
diff --git a/lib/material-groups/table/material-group-table-columns.tsx b/lib/material-groups/table/material-group-table-columns.tsx
new file mode 100644
index 00000000..90df720d
--- /dev/null
+++ b/lib/material-groups/table/material-group-table-columns.tsx
@@ -0,0 +1,61 @@
+"use client"
+
+import * as React from "react"
+import { type ColumnDef } from "@tanstack/react-table"
+
+import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
+
+// MaterialGroup 타입 정의 (서비스에서 반환되는 타입과 일치)
+type MaterialGroup = {
+ materialGroupCode: string | null;
+ materialName: string | null;
+ displayText: string | null;
+}
+
+/**
+ * MaterialGroup 테이블 컬럼 정의
+ */
+export function getColumns(): ColumnDef<MaterialGroup>[] {
+ // ----------------------------------------------------------------
+ // 데이터 컬럼들
+ // ----------------------------------------------------------------
+ const dataColumns: ColumnDef<MaterialGroup>[] = [
+ {
+ accessorKey: "materialGroupCode",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="자재그룹코드" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("materialGroupCode") as string | null
+ return (
+ <div className="font-medium">
+ {value || "-"}
+ </div>
+ )
+ },
+ enableSorting: true,
+ enableHiding: false,
+ },
+ {
+ accessorKey: "materialName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="자재그룹명" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("materialName") as string | null
+ return (
+ <div className="max-w-[300px] truncate">
+ {value || "-"}
+ </div>
+ )
+ },
+ enableSorting: true,
+ enableHiding: false,
+ },
+ ]
+
+ // ----------------------------------------------------------------
+ // 최종 컬럼 배열
+ // ----------------------------------------------------------------
+ return dataColumns
+}
diff --git a/lib/material-groups/table/material-group-table.tsx b/lib/material-groups/table/material-group-table.tsx
new file mode 100644
index 00000000..21430c4f
--- /dev/null
+++ b/lib/material-groups/table/material-group-table.tsx
@@ -0,0 +1,178 @@
+"use client"
+
+import * as React from "react"
+import type {
+ DataTableAdvancedFilterField,
+ DataTableFilterField,
+} from "@/types/table"
+
+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 { Button } from "@/components/ui/button"
+import { Alert, AlertDescription } from "@/components/ui/alert"
+
+import { getMaterialGroups } from "../services"
+import { getColumns } from "./material-group-table-columns"
+import { ViewModeToggle } from "@/components/data-table/view-mode-toggle"
+
+// MaterialGroup 타입 정의 (서비스에서 반환되는 타입과 일치)
+type MaterialGroup = {
+ materialGroupCode: string | null;
+ materialName: string | null;
+ displayText: string | null;
+}
+
+interface MaterialGroupTableProps {
+ promises?: Promise<
+ [
+ Awaited<ReturnType<typeof getMaterialGroups>>,
+ ]
+ >
+}
+
+export function MaterialGroupTable({ promises }: MaterialGroupTableProps) {
+ // 페이지네이션 모드 데이터
+ const paginationData = promises ? React.use(promises) : null
+ const [{ data = [], pageCount = 0 }] = paginationData || [{ data: [], pageCount: 0 }]
+
+ console.log('MaterialGroupTable data:', data.length, 'material groups')
+
+ const columns = React.useMemo(
+ () => getColumns(),
+ []
+ )
+
+ // 기존 필터 필드들
+ const filterFields: DataTableFilterField<MaterialGroup>[] = [
+ {
+ id: "materialGroupCode",
+ label: "자재그룹코드",
+ },
+ {
+ id: "materialName",
+ label: "자재그룹명",
+ },
+ ]
+
+ const advancedFilterFields: DataTableAdvancedFilterField<MaterialGroup>[] = [
+ {
+ id: "materialGroupCode",
+ label: "자재그룹코드",
+ type: "text",
+ },
+ {
+ id: "materialName",
+ label: "자재그룹명",
+ type: "text",
+ },
+ {
+ id: "displayText",
+ label: "표시텍스트",
+ type: "text",
+ },
+ ]
+
+ // 확장된 useDataTable 훅 사용 (pageSize 기반 자동 전환)
+ const {
+ table,
+ infiniteScroll,
+ isInfiniteMode,
+ handlePageSizeChange,
+ } = useDataTable({
+ data,
+ columns,
+ pageCount,
+ filterFields,
+ enablePinning: true,
+ enableAdvancedFilter: true,
+ initialState: {
+ sorting: [{ id: "materialGroupCode", desc: false }],
+ },
+ getRowId: (originalRow) => String(originalRow.materialGroupCode || 'unknown'),
+ shallow: false,
+ clearOnDefault: true,
+ // 무한 스크롤 설정
+ infiniteScrollConfig: {
+ apiEndpoint: "/api/table/material-groups/infinite",
+ tableName: "material-groups",
+ maxPageSize: 100,
+ },
+ })
+
+ return (
+ <div className="w-full space-y-2.5 overflow-x-auto" style={{maxWidth:'100vw'}}>
+
+ {/* 모드 토글 */}
+ <div className="flex items-center justify-between">
+ <ViewModeToggle
+ isInfiniteMode={isInfiniteMode}
+ onSwitch={handlePageSizeChange}
+ />
+ </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}
+ />
+
+ {/* 테이블 렌더링 */}
+ {isInfiniteMode ? (
+ // 무한 스크롤 모드: InfiniteDataTable 사용
+ <InfiniteDataTable
+ table={table}
+ hasNextPage={infiniteScroll?.hasNextPage || false}
+ isLoadingMore={infiniteScroll?.isLoadingMore || false}
+ onLoadMore={infiniteScroll?.onLoadMore}
+ 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>
+ )}
+
+ </div>
+ )
+}
diff --git a/lib/material-groups/validations.ts b/lib/material-groups/validations.ts
new file mode 100644
index 00000000..c379c833
--- /dev/null
+++ b/lib/material-groups/validations.ts
@@ -0,0 +1,31 @@
+import {
+ createSearchParamsCache,
+ parseAsArrayOf,
+ parseAsInteger,
+ parseAsString,
+ parseAsStringEnum,
+} from "nuqs/server"
+import * as z from "zod"
+
+import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers"
+import { materialSearchView } from "@/db/schema/items"
+
+// MaterialGroup 검색 파라미터 캐시 정의
+export const searchParamsCache = createSearchParamsCache({
+ flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault([]),
+ page: parseAsInteger.withDefault(1),
+ perPage: parseAsInteger.withDefault(10),
+
+ // 확장된 타입으로 정렬 파서 사용
+ sort: getSortingStateParser<typeof materialSearchView>().withDefault([
+ { id: "materialGroupCode", desc: false },
+ ]),
+
+ // 고급 필터
+ filters: getFiltersStateParser().withDefault([]),
+ joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"),
+ search: parseAsString.withDefault(""),
+});
+
+// 타입 내보내기
+export type GetMaterialGroupsSchema = Awaited<ReturnType<typeof searchParamsCache.parse>>;