diff options
| author | joonhoekim <26rote@gmail.com> | 2025-09-15 01:23:00 +0000 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-09-15 01:23:00 +0000 |
| commit | e7818a457371849e29519497ebf046f385f05ab6 (patch) | |
| tree | 9bf08ba1b31a512c481dc521c9dd7c90091a75b8 /lib/avl/table/standard-avl-table.tsx | |
| parent | 3f293c90beb58ce206a66ff444d7acfc41b56429 (diff) | |
(김준회) AVL 기능 구현 1차 및 벤더풀 E/B 구분 개선
Diffstat (limited to 'lib/avl/table/standard-avl-table.tsx')
| -rw-r--r-- | lib/avl/table/standard-avl-table.tsx | 380 |
1 files changed, 380 insertions, 0 deletions
diff --git a/lib/avl/table/standard-avl-table.tsx b/lib/avl/table/standard-avl-table.tsx new file mode 100644 index 00000000..924b972a --- /dev/null +++ b/lib/avl/table/standard-avl-table.tsx @@ -0,0 +1,380 @@ +"use client" + +import * as React from "react" +import { useReactTable, getCoreRowModel, getPaginationRowModel, getSortedRowModel, getFilteredRowModel, type ColumnDef } from "@tanstack/react-table" +import { DataTable } from "@/components/data-table/data-table" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { getStandardAvlVendorInfo } from "../service" +import { GetStandardAvlSchema } from "../validations" +import { AvlDetailItem } from "../types" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Search } from "lucide-react" +import { toast } from "sonner" + +// 검색 옵션들 +const constructionSectorOptions = [ + { value: "all", label: "전체" }, + { value: "조선", label: "조선" }, + { value: "해양", label: "해양" }, +] + +const shipTypeOptions = [ + { value: "all", label: "전체" }, + { value: "컨테이너선", label: "컨테이너선" }, + { value: "유조선", label: "유조선" }, + { value: "LNG선", label: "LNG선" }, + { value: "LPG선", label: "LPG선" }, + { value: "벌크선", label: "벌크선" }, + { value: "여객선", label: "여객선" }, +] + +const avlKindOptions = [ + { value: "all", label: "전체" }, + { value: "표준", label: "표준" }, + { value: "특별", label: "특별" }, + { value: "임시", label: "임시" }, +] + +const htDivisionOptions = [ + { value: "all", label: "전체" }, + { value: "H", label: "Hull (H)" }, + { value: "T", label: "Topside (T)" }, +] + +// 선종별 표준 AVL 테이블에서는 AvlDetailItem을 사용 +export type StandardAvlItem = AvlDetailItem + +interface StandardAvlTableProps { + onSelectionChange?: (count: number) => void + resetCounter?: number + constructionSector?: string // 공사부문 필터 + shipType?: string // 선종 필터 + avlKind?: string // AVL 종류 필터 + htDivision?: string // H/T 구분 필터 +} + +// 선종별 표준 AVL 테이블 컬럼 +const standardAvlColumns: ColumnDef<StandardAvlItem>[] = [ + { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row, table }) => { + // 선종별 표준 AVL 테이블의 단일 선택 핸들러 + const handleRowSelection = (checked: boolean) => { + if (checked) { + // 다른 모든 행의 선택 해제 + table.getRowModel().rows.forEach(r => { + if (r !== row && r.getIsSelected()) { + r.toggleSelected(false) + } + }) + } + // 현재 행 선택/해제 + row.toggleSelected(checked) + } + + return ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={handleRowSelection} + aria-label="Select row" + /> + ) + }, + enableSorting: false, + enableHiding: false, + size: 50, + }, + { + accessorKey: "no", + header: "No.", + size: 60, + }, + { + accessorKey: "disciplineName", + header: "설계공종", + size: 120, + }, + { + accessorKey: "avlVendorName", + header: "AVL 등재업체명", + size: 140, + }, + { + accessorKey: "materialGroupCode", + header: "자재그룹 코드", + size: 120, + }, + { + accessorKey: "materialGroupName", + header: "자재그룹 명", + size: 130, + }, + { + accessorKey: "vendorCode", + header: "협력업체 코드", + size: 120, + }, + { + accessorKey: "vendorName", + header: "협력업체 명", + size: 130, + }, + { + accessorKey: "headquarterLocation", + header: "본사 위치 (국가)", + size: 140, + }, + { + accessorKey: "tier", + header: "등급 (Tier)", + size: 120, + }, +] + +export function StandardAvlTable({ + onSelectionChange, + resetCounter, + constructionSector: initialConstructionSector, + shipType: initialShipType, + avlKind: initialAvlKind, + htDivision: initialHtDivision +}: StandardAvlTableProps) { + const [data, setData] = React.useState<StandardAvlItem[]>([]) + const [loading, setLoading] = React.useState(false) + const [pageCount, setPageCount] = React.useState(0) + + // 검색 상태 + const [searchConstructionSector, setSearchConstructionSector] = React.useState(initialConstructionSector || "all") + const [searchShipType, setSearchShipType] = React.useState(initialShipType || "all") + const [searchAvlKind, setSearchAvlKind] = React.useState(initialAvlKind || "all") + const [searchHtDivision, setSearchHtDivision] = React.useState(initialHtDivision || "all") + + // 데이터 로드 함수 + const loadData = React.useCallback(async (searchParams: Partial<GetStandardAvlSchema>) => { + try { + setLoading(true) + const params: GetStandardAvlSchema = { + page: searchParams.page ?? 1, + perPage: searchParams.perPage ?? 10, + sort: searchParams.sort ?? [{ id: "no", desc: false }], + constructionSector: searchConstructionSector === "all" ? "" : searchConstructionSector || "", + shipType: searchShipType === "all" ? "" : searchShipType || "", + avlKind: searchAvlKind === "all" ? "" : searchAvlKind || "", + htDivision: searchHtDivision === "all" ? "" : searchHtDivision || "", + search: "", + ...searchParams, + } + const result = await getStandardAvlVendorInfo(params) + setData(result.data) + setPageCount(result.pageCount) + } catch (error) { + console.error("선종별 표준 AVL 데이터 로드 실패:", error) + setData([]) + setPageCount(0) + } finally { + setLoading(false) + } + }, [searchConstructionSector, searchShipType, searchAvlKind, searchHtDivision]) + + // 검색 핸들러 + const handleSearch = React.useCallback(() => { + loadData({}) + }, [loadData]) + + // 검색 초기화 핸들러 + const handleResetSearch = React.useCallback(() => { + setSearchConstructionSector("all") + setSearchShipType("all") + setSearchAvlKind("all") + setSearchHtDivision("all") + loadData({}) + }, [loadData]) + + // 초기 데이터 로드 + React.useEffect(() => { + loadData({}) + }, [loadData]) + + const table = useReactTable({ + data, + columns: standardAvlColumns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + manualPagination: true, + pageCount, + initialState: { + pagination: { + pageSize: 10, + }, + }, + onPaginationChange: (updater) => { + const newState = typeof updater === 'function' ? updater(table.getState().pagination) : updater + loadData({ + page: newState.pageIndex + 1, + perPage: newState.pageSize, + }) + }, + }) + + // 선택 상태 변경 시 콜백 호출 + React.useEffect(() => { + onSelectionChange?.(table.getFilteredSelectedRowModel().rows.length) + }, [table.getFilteredSelectedRowModel().rows.length, onSelectionChange]) + + // 선택 해제 요청이 오면 모든 선택 해제 + React.useEffect(() => { + if (resetCounter && resetCounter > 0) { + table.toggleAllPageRowsSelected(false) + } + }, [resetCounter, table]) + + return ( + <div className="h-full flex flex-col"> + <div className="mb-2"> + <div className="flex items-center justify-between mb-2"> + <h4 className="font-medium">선종별 표준 AVL</h4> + <div className="flex gap-1"> + <Button variant="outline" size="sm" onClick={() => toast.info("개발 중입니다.")}> + 신규업체 추가 + </Button> + <Button variant="outline" size="sm" onClick={() => toast.info("개발 중입니다.")}> + 파일 업로드 + </Button> + <Button variant="outline" size="sm" onClick={() => toast.info("개발 중입니다.")}> + 일괄입력 + </Button> + <Button variant="outline" size="sm" onClick={() => toast.info("개발 중입니다.")}> + 항목삭제 + </Button> + <Button variant="outline" size="sm" onClick={() => toast.info("개발 중입니다.")}> + 저장 + </Button> + <Button variant="outline" size="sm" onClick={() => toast.info("개발 중입니다.")}> + 최종 확정 + </Button> + </div> + </div> + </div> + + {/* 검색 UI */} + <div className="mb-4 p-4 border rounded-lg bg-muted/50"> + <div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-4"> + {/* 공사부문 */} + <div className="space-y-2"> + <label className="text-sm font-medium">공사부문</label> + <Select value={searchConstructionSector} onValueChange={setSearchConstructionSector}> + <SelectTrigger> + <SelectValue /> + </SelectTrigger> + <SelectContent> + {constructionSectorOptions.map((option) => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + + {/* 선종 */} + <div className="space-y-2"> + <label className="text-sm font-medium">선종</label> + <Select value={searchShipType} onValueChange={setSearchShipType}> + <SelectTrigger> + <SelectValue /> + </SelectTrigger> + <SelectContent> + {shipTypeOptions.map((option) => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + + {/* AVL종류 */} + <div className="space-y-2"> + <label className="text-sm font-medium">AVL종류</label> + <Select value={searchAvlKind} onValueChange={setSearchAvlKind}> + <SelectTrigger> + <SelectValue /> + </SelectTrigger> + <SelectContent> + {avlKindOptions.map((option) => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + + {/* H/T */} + <div className="space-y-2"> + <label className="text-sm font-medium">H/T 구분</label> + <Select value={searchHtDivision} onValueChange={setSearchHtDivision}> + <SelectTrigger> + <SelectValue /> + </SelectTrigger> + <SelectContent> + {htDivisionOptions.map((option) => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + + {/* 검색 버튼들 */} + <div className="space-y-2"> + <label className="text-sm font-medium opacity-0">버튼</label> + <div className="flex gap-1"> + <Button + onClick={handleSearch} + disabled={loading} + size="sm" + className="px-3" + > + <Search className="w-4 h-4 mr-1" /> + 조회 + </Button> + <Button + onClick={handleResetSearch} + variant="outline" + size="sm" + className="px-3" + > + 초기화 + </Button> + </div> + </div> + </div> + </div> + + <div className="flex-1"> + <DataTable table={table} /> + </div> + </div> + ) +} |
