From f72142f6cc46c7be5bf90803d365c2ecd144c53d Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Mon, 1 Sep 2025 10:22:55 +0000 Subject: (김준회) MDG 자재마스터 정보 조회 기능 및 메뉴 추가, 회원가입시 공급품목 선택 기능 추가 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/material/table/material-detail-dialog.tsx | 359 ++++++++++++++++++++++++++ lib/material/table/material-table-columns.tsx | 183 +++++++++++++ lib/material/table/material-table.tsx | 199 ++++++++++++++ 3 files changed, 741 insertions(+) create mode 100644 lib/material/table/material-detail-dialog.tsx create mode 100644 lib/material/table/material-table-columns.tsx create mode 100644 lib/material/table/material-table.tsx (limited to 'lib/material/table') diff --git a/lib/material/table/material-detail-dialog.tsx b/lib/material/table/material-detail-dialog.tsx new file mode 100644 index 00000000..aed0485c --- /dev/null +++ b/lib/material/table/material-detail-dialog.tsx @@ -0,0 +1,359 @@ +"use client" + +import * as React from "react" +import { getMaterialDetail } from "../services" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/tabs" +import { Skeleton } from "@/components/ui/skeleton" +import { Badge } from "@/components/ui/badge" + +import { formatDate } from "@/lib/utils" + +interface MaterialDetailDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + matnr: string | null +} + +export function MaterialDetailDialog({ + open, + onOpenChange, + matnr, +}: MaterialDetailDialogProps) { + const [data, setData] = React.useState> | null>(null) + const [loading, setLoading] = React.useState(false) + + React.useEffect(() => { + if (open && matnr) { + setLoading(true) + getMaterialDetail(matnr) + .then(setData) + .finally(() => setLoading(false)) + } + }, [open, matnr]) + + if (!matnr) return null + + return ( + + + + 자재마스터 상세정보 + + 자재코드: {matnr} + + + + {loading ? ( +
+ + + +
+ ) : data ? ( + + + 기본정보 + 특성할당 + 클래스할당 + 설명 + 단위 + + +
+ + +
+
+

기본 자재 정보

+ + + + 자재코드 (MATNR) + {data.material.MATNR || "-"} + + + 자재명 (ZZNAME) + {data.material.ZZNAME || "-"} + + + 프로젝트 (ZZPJT) + {data.material.ZZPJT || "-"} + + + 구 자재번호 (BISMT) + {data.material.BISMT || "-"} + + + 자재그룹 (MATKL) + {data.material.MATKL || "-"} + + + 자재유형 (MTART) + {data.material.MTART || "-"} + + + 기본단위 (MEINS) + {data.material.MEINS || "-"} + + + 산업섹터 (MBRSH) + {data.material.MBRSH || "-"} + + + 사업부 (SPART) + {data.material.SPART || "-"} + + + 규격 (ZZSPEC) + {data.material.ZZSPEC || "-"} + + + 설명 (ZZDESC) + {data.material.ZZDESC || "-"} + + + PLM ID (ZZPLMID) + {data.material.ZZPLMID || "-"} + + + 삭제플래그 (LVORM) + + {data.material.LVORM ? ( + 삭제됨 + ) : ( + 활성 + )} + + + + 자재상태 (MSTAE) + {data.material.MSTAE || "-"} + + + 총 중량 (BRGEW) + {data.material.BRGEW || "-"} + + + 순 중량 (NTGEW) + {data.material.NTGEW || "-"} + + + 중량단위 (GEWEI) + {data.material.GEWEI || "-"} + + + 크기/치수 (GROES) + {data.material.GROES || "-"} + + + 생성일시 + {formatDate(data.material.createdAt, "KR")} + + + 수정일시 + {formatDate(data.material.updatedAt, "KR")} + + +
+
+
+
+ + +
+

특성 할당 정보

+ {data.characteristics.length === 0 && ( +

특성 할당 정보가 없습니다.

+ )} + + + + 특성명 (ATNAM) + 특성값 (ATWRT) + 특성내역 (ATBEZ) + 특성값내역 (ATWTB) + 클래스번호 (CLASS) + 클래스유형 (KLART) + 측정단위 (ATAWE) + + + + {data.characteristics.length > 0 ? ( + data.characteristics.map((char, index) => ( + + {char.ATNAM || "-"} + {char.ATWRT || "-"} + {char.ATBEZ || "-"} + {char.ATWTB || "-"} + {char.CLASS || "-"} + {char.KLART || "-"} + {char.ATAWE || "-"} + + )) + ) : ( + + + 데이터가 없습니다 + + + )} + +
+
+
+ + +
+

클래스 할당 정보

+ {data.classifications.length === 0 && ( +

클래스 할당 정보가 없습니다.

+ )} + + + + 클래스번호 (CLASS) + 클래스유형 (KLART) + 생성일시 + 수정일시 + + + + {data.classifications.length > 0 ? ( + data.classifications.map((cls, index) => ( + + {cls.CLASS || "-"} + {cls.KLART || "-"} + {formatDate(cls.createdAt, "KR")} + {formatDate(cls.updatedAt, "KR")} + + )) + ) : ( + + + 데이터가 없습니다 + + + )} + +
+
+
+ + +
+

자재 설명 정보

+ {data.descriptions.length === 0 && ( +

자재 설명 정보가 없습니다.

+ )} + + + + 자재설명 (MAKTX) + 언어 (SPRAS) + 생성일시 + 수정일시 + + + + {data.descriptions.length > 0 ? ( + data.descriptions.map((desc, index) => ( + + {desc.MAKTX || "-"} + {desc.SPRAS || "-"} + {formatDate(desc.createdAt, "KR")} + {formatDate(desc.updatedAt, "KR")} + + )) + ) : ( + + + 데이터가 없습니다 + + + )} + +
+
+
+ + +
+

단위 정보

+ {data.units.length === 0 && ( +

단위 정보가 없습니다.

+ )} + + + + 대체단위 (MEINH) + 분모 (UMREN) + 분자 (UMREZ) + 길이 (LAENG) + 폭 (BREIT) + 높이 (HOEHE) + 부피 (VOLUM) + 부피단위 (VOLEH) + 총중량 (BRGEW) + 중량단위 (GEWEI) + 치수단위 (MEABM) + + + + {data.units.length > 0 ? ( + data.units.map((unit, index) => ( + + {unit.MEINH || "-"} + {unit.UMREN || "-"} + {unit.UMREZ || "-"} + {unit.LAENG || "-"} + {unit.BREIT || "-"} + {unit.HOEHE || "-"} + {unit.VOLUM || "-"} + {unit.VOLEH || "-"} + {unit.BRGEW || "-"} + {unit.GEWEI || "-"} + {unit.MEABM || "-"} + + )) + ) : ( + + + 데이터가 없습니다 + + + )} + +
+
+
+
+
+ ) : ( +
+

데이터를 찾을 수 없습니다.

+
+ )} +
+
+ ) +} diff --git a/lib/material/table/material-table-columns.tsx b/lib/material/table/material-table-columns.tsx new file mode 100644 index 00000000..dd405770 --- /dev/null +++ b/lib/material/table/material-table-columns.tsx @@ -0,0 +1,183 @@ +"use client" + +import * as React from "react" +import { type ColumnDef } from "@tanstack/react-table" +import { type DataTableRowAction } from "@/types/table" + +import { formatDate } from "@/lib/utils" +import { Checkbox } from "@/components/ui/checkbox" +import { Button } from "@/components/ui/button" +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" + +// Material 타입 정의 (서비스에서 반환되는 타입과 일치) +type Material = { + id: number; + MATKL: string | null; // 자재그룹 + MATNR: string | null; // 자재코드 + ZZNAME: string | null; // 자재명 + ZZPJT: string | null; // 프로젝트 + createdAt: Date; + updatedAt: Date; +} + +interface GetColumnsProps { + setRowAction: React.Dispatch | null>> +} + +/** + * Material 테이블 컬럼 정의 + */ +export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef[] { + + // ---------------------------------------------------------------- + // 1) select 컬럼 (체크박스) + // ---------------------------------------------------------------- + const selectColumn: ColumnDef = { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + className="translate-y-0.5" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + className="translate-y-0.5" + /> + ), + size: 40, + enableSorting: false, + enableHiding: false, + } + + // ---------------------------------------------------------------- + // 2) 데이터 컬럼들 + // ---------------------------------------------------------------- + const dataColumns: ColumnDef[] = [ + { + accessorKey: "MATKL", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const value = row.getValue("MATKL") as string | null + return ( +
+ {value || "-"} +
+ ) + }, + enableSorting: true, + enableHiding: false, + }, + { + accessorKey: "MATNR", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const value = row.getValue("MATNR") as string | null + return ( +
+ {value || "-"} +
+ ) + }, + enableSorting: true, + enableHiding: false, + }, + { + accessorKey: "ZZNAME", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const value = row.getValue("ZZNAME") as string | null + return ( +
+ {value || "-"} +
+ ) + }, + enableSorting: true, + }, + { + accessorKey: "ZZPJT", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const value = row.getValue("ZZPJT") as string | null + return ( +
+ {value || "-"} +
+ ) + }, + enableSorting: true, + }, + { + accessorKey: "createdAt", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const value = row.getValue("createdAt") as Date + return ( +
+ {formatDate(value, "KR")} +
+ ) + }, + enableSorting: true, + }, + { + accessorKey: "updatedAt", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const value = row.getValue("updatedAt") as Date + return ( +
+ {formatDate(value, "KR")} +
+ ) + }, + enableSorting: true, + }, + ] + + // ---------------------------------------------------------------- + // 3) actions 컬럼 (상세보기) + // ---------------------------------------------------------------- + const actionsColumn: ColumnDef = { + id: "actions", + enableHiding: false, + cell: function Cell({ row }) { + return ( + + ) + }, + size: 40, + } + + // ---------------------------------------------------------------- + // 4) 최종 컬럼 배열 + // ---------------------------------------------------------------- + return [ + selectColumn, + ...dataColumns, + actionsColumn, + ] +} diff --git a/lib/material/table/material-table.tsx b/lib/material/table/material-table.tsx new file mode 100644 index 00000000..6870a030 --- /dev/null +++ b/lib/material/table/material-table.tsx @@ -0,0 +1,199 @@ +"use client" + +import * as React from "react" +import type { + DataTableAdvancedFilterField, + DataTableFilterField, + DataTableRowAction, +} 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 { getMaterials } from "../services" +import { getColumns } from "./material-table-columns" +import { MaterialDetailDialog } from "./material-detail-dialog" +import { ViewModeToggle } from "@/components/data-table/view-mode-toggle" + +// Material 타입 정의 (서비스에서 반환되는 타입과 일치) +type Material = { + id: number; + MATKL: string | null; // 자재그룹(=자재그룹코드) + MATNR: string | null; // 자재코드 + ZZNAME: string | null; // 자재명 + ZZPJT: string | null; // 프로젝트 + createdAt: Date; + updatedAt: Date; +} + +interface MaterialTableProps { + promises?: Promise< + [ + Awaited>, + ] + > +} + +export function MaterialTable({ promises }: MaterialTableProps) { + // 페이지네이션 모드 데이터 + const paginationData = promises ? React.use(promises) : null + const [{ data = [], pageCount = 0 }] = paginationData || [{ data: [], pageCount: 0 }] + + console.log('MaterialTable data:', data.length, 'materials') + + const [rowAction, setRowAction] = + React.useState | null>(null) + + const columns = React.useMemo( + () => getColumns({ setRowAction }), + [setRowAction] + ) + + // 기존 필터 필드들 + const filterFields: DataTableFilterField[] = [ + { + id: "MATKL", + label: "자재그룹", + }, + { + id: "MATNR", + label: "자재코드", + }, + ] + + const advancedFilterFields: DataTableAdvancedFilterField[] = [ + { + id: "MATKL", + label: "자재그룹", + type: "text", + }, + { + id: "MATNR", + label: "자재코드", + type: "text", + }, + { + id: "ZZNAME", + label: "자재명", + type: "text", + }, + { + id: "ZZPJT", + label: "프로젝트", + type: "text", + }, + ] + + // 확장된 useDataTable 훅 사용 (pageSize 기반 자동 전환) + const { + table, + infiniteScroll, + isInfiniteMode, + handlePageSizeChange, + } = useDataTable({ + data, + columns, + pageCount, + filterFields, + enablePinning: true, + enableAdvancedFilter: true, + initialState: { + sorting: [{ id: "createdAt", desc: true }], + columnPinning: { left: ["select"], right: ["actions"] }, + }, + getRowId: (originalRow) => String(originalRow.id), + shallow: false, + clearOnDefault: true, + // 무한 스크롤 설정 + infiniteScrollConfig: { + apiEndpoint: "/api/table/materials/infinite", + tableName: "materials", + maxPageSize: 100, + }, + }) + + return ( +
+ + {/* 모드 토글 */} +
+ +
+ + {/* 에러 상태 (무한 스크롤 모드) */} + {isInfiniteMode && infiniteScroll?.error && ( + + + 데이터를 불러오는 중 오류가 발생했습니다. + + + + )} + + {/* 로딩 상태가 아닐 때만 테이블 렌더링 */} + {!(isInfiniteMode && infiniteScroll?.isLoading && infiniteScroll?.isEmpty) ? ( + <> + {/* 도구 모음 */} + + + {/* 테이블 렌더링 */} + {isInfiniteMode ? ( + // 무한 스크롤 모드: InfiniteDataTable 사용 + + ) : ( + // 페이지네이션 모드: DataTable 사용 + + )} + + ) : ( + /* 로딩 스켈레톤 (무한 스크롤 초기 로딩) */ +
+
+ 무한 스크롤 모드로 데이터를 로드하고 있습니다... +
+ {Array.from({ length: 10 }).map((_, i) => ( +
+ ))} +
+ )} + + {/* 상세보기 다이얼로그 */} + setRowAction(null)} + matnr={rowAction?.row.original?.MATNR || null} + /> +
+ ) +} -- cgit v1.2.3