diff options
Diffstat (limited to 'app')
| -rw-r--r-- | app/[lng]/evcp/(evcp)/avl/[id]/page.tsx | 106 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/avl/avl-page-client.tsx | 107 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/avl/components/avl-registration-area.tsx | 792 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/avl/page.tsx | 49 |
4 files changed, 1054 insertions, 0 deletions
diff --git a/app/[lng]/evcp/(evcp)/avl/[id]/page.tsx b/app/[lng]/evcp/(evcp)/avl/[id]/page.tsx new file mode 100644 index 00000000..52ee7b7f --- /dev/null +++ b/app/[lng]/evcp/(evcp)/avl/[id]/page.tsx @@ -0,0 +1,106 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" +import { notFound } from "next/navigation" + +import { getValidFilters } from "@/lib/data-table" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { getAvlLists, getAvlDetail } from "@/lib/avl/service" +import { avlDetailSearchParamsCache } from "@/lib/avl/validations" +import { AvlDetailTable } from "@/lib/avl/table/avl-detail-table" +import { getAvlListById } from "@/lib/avl/service" + +interface AvlDetailPageProps { + params: Promise<{ id: string }> + searchParams: Promise<SearchParams> +} + +export default async function AvlDetailPage(props: AvlDetailPageProps) { + const { id } = await props.params + const searchParams = await props.searchParams + const search = avlDetailSearchParamsCache.parse(searchParams) + + const validFilters = getValidFilters(search.filters) + + // ID 검증 + const numericId = Number(id) + if (isNaN(numericId) || numericId <= 0) { + notFound() + } + + // AVL 리스트 정보 조회 + const avlListInfo = await getAvlListById(numericId) + if (!avlListInfo) { + notFound() + } + + const promises = Promise.all([ + getAvlDetail({ + ...search, + filters: validFilters, + avlListId: numericId, + }), + ]) + + return ( + <div className="h-screen flex flex-col"> + {/* 메인 콘텐츠 영역 */} + <div className="flex-1 overflow-hidden"> + <div className="h-full p-4 md:p-6"> + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={20} + searchableColumnCount={1} + filterableColumnCount={5} + cellWidths={[ + "50px", "60px", "120px", "120px", "150px", "130px", "130px", + "140px", "140px", "100px", "100px", "100px", "100px", "120px", + "140px", "150px", "80px", "100px", "80px", "140px", "120px", + "160px", "100px", "120px", "120px", "130px", "120px", "130px", "200px" + ]} + shrinkZero + /> + } + > + <AvlDetailTableWrapper + promises={promises} + avlListId={Number(id)} + avlListInfo={avlListInfo} + /> + </React.Suspense> + </div> + </div> + </div> + ) +} + +// 실제 데이터를 받아서 AvlDetailTable에 전달하는 컴포넌트 +function AvlDetailTableWrapper({ + promises, + avlListId, + avlListInfo +}: { + promises: Promise<any> + avlListId: number + avlListInfo: any +}) { + const [{ data, pageCount }] = React.use(promises) + + // AVL 타입 결정 + const avlType = avlListInfo.isTemplate ? '선종별표준AVL' : '프로젝트AVL' + + // 선주명 추출 (프로젝트 정보에서) + const shipOwnerName = avlListInfo.shipOwnerName || undefined + + return ( + <AvlDetailTable + data={data} + pageCount={pageCount} + avlListId={avlListId} + avlType={avlType} + projectCode={avlListInfo.projectCode} + shipOwnerName={shipOwnerName} + businessType={avlListInfo.constructionSector || '조선'} + /> + ) +} diff --git a/app/[lng]/evcp/(evcp)/avl/avl-page-client.tsx b/app/[lng]/evcp/(evcp)/avl/avl-page-client.tsx new file mode 100644 index 00000000..a9d5713d --- /dev/null +++ b/app/[lng]/evcp/(evcp)/avl/avl-page-client.tsx @@ -0,0 +1,107 @@ +"use client" + +import { useEffect, useState } from "react" +import { + ResizablePanelGroup, + ResizablePanel, + ResizableHandle, +} from "@/components/ui/resizable" +import { AvlTable } from "@/lib/avl/table/avl-table" +import { AvlRegistrationArea } from "@/lib/avl/table/avl-registration-area" +import { getAvlLists } from "@/lib/avl/service" +import { AvlListItem } from "@/lib/avl/types" +import { toast } from "sonner" + +interface AvlPageClientProps { + initialData: AvlListItem[] +} + +export function AvlPageClient({ initialData }: AvlPageClientProps) { + const [avlListData, setAvlListData] = useState<AvlListItem[]>(initialData) + const [isLoading, setIsLoading] = useState(false) + const [registrationMode, setRegistrationMode] = useState<'standard' | 'project' | null>(null) + const [selectedAvlRow, setSelectedAvlRow] = useState<AvlListItem | null>(null) + + // 초기 데이터 설정 + useEffect(() => { + setAvlListData(initialData) + }, [initialData]) + + // AVL 리스트 데이터 로드 (클라이언트에서 추가 로드 시 사용) + const loadAvlListData = async () => { + try { + setIsLoading(true) + // 기본 파라미터로 전체 데이터 조회 + const result = await getAvlLists({ + page: 1, + perPage: 100, // 충분한 수량으로 조회 + sort: [{ id: "createdAt", desc: true }], + flags: [], + filters: [], + joinOperator: "and", + search: "", + isTemplate: "false", + constructionSector: "", + projectCode: "", + shipType: "", + avlKind: "", + htDivision: "H", + rev: "", + }) + + setAvlListData(result.data) + } catch (error) { + console.error("AVL 리스트 로드 실패:", error) + toast.error("AVL 리스트를 불러오는데 실패했습니다.") + setAvlListData([]) + } finally { + setIsLoading(false) + } + } + + // 리프레시 핸들러 + const handleRefresh = async () => { + await loadAvlListData() + toast.success("AVL 리스트가 새로고침되었습니다.") + } + + // 등록 모드 변경 핸들러 + const handleRegistrationModeChange = (mode: 'standard' | 'project') => { + setRegistrationMode(mode) + } + + // 행 선택 핸들러 + const handleRowSelect = (selectedRow: AvlListItem | null) => { + setSelectedAvlRow(selectedRow) + } + + return ( + <div className="h-screen flex flex-col"> + <div className="flex-1 overflow-hidden"> + <ResizablePanelGroup direction="vertical" className="h-full"> + {/* 상단 패널: AVL 목록 */} + <ResizablePanel defaultSize={40} minSize={20}> + <div className="h-full p-4"> + <AvlTable + data={avlListData} + onRefresh={handleRefresh} + isLoading={isLoading} + onRegistrationModeChange={handleRegistrationModeChange} + onRowSelect={handleRowSelect} + /> + </div> + </ResizablePanel> + + <ResizableHandle withHandle /> + + {/* 하단 패널: AVL 등록 */} + <ResizablePanel defaultSize={60} minSize={30}> + <div className="h-full p-4 overflow-x-auto overflow-y-hidden"> + <AvlRegistrationArea disabled={registrationMode === null} /> + </div> + </ResizablePanel> + </ResizablePanelGroup> + </div> + </div> + ) +} diff --git a/app/[lng]/evcp/(evcp)/avl/components/avl-registration-area.tsx b/app/[lng]/evcp/(evcp)/avl/components/avl-registration-area.tsx new file mode 100644 index 00000000..69b1d417 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/avl/components/avl-registration-area.tsx @@ -0,0 +1,792 @@ +"use client" + +import * as React from "react" +import { Button } from "@/components/ui/button" +import { Card } from "@/components/ui/card" +import { Checkbox } from "@/components/ui/checkbox" +import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react" +import { + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, + type ColumnDef, +} from "@tanstack/react-table" +import { DataTable } from "@/components/data-table/data-table" + +// 프로젝트 AVL 데이터 타입 +type ProjectAvlItem = { + id: string + no: number + designCategory: string + customerAvlMaterialName: string + materialGroupInfo: string + avlVendorName: string + vendorInfo: string + ownerSuggestion: string + shiSuggestion: string +} + +// 선종별 표준 AVL 데이터 타입 +type StandardAvlItem = { + id: string + no: number + designCategory: string + avlVendorName: string + materialGroupInfo: string + vendorInfo: string + headquarterLocation: string + tier: string +} + +// Vendor Pool 데이터 타입 +type VendorPoolItem = { + id: string + no: number + designCategory: string + avlVendorName: string + materialGroupInfo: string + vendorInfo: string + vendorClassification: string + faStatus: string + recentQuoteNumber: string + recentOrderNumber: string +} + +// Mock 데이터들 +const mockProjectAvlData: ProjectAvlItem[] = [ + { + id: "p1", + no: 1, + designCategory: "엔진", + customerAvlMaterialName: "메인엔진", + materialGroupInfo: "엔진부품", + avlVendorName: "엔진업체A", + vendorInfo: "국내 엔진 전문업체", + ownerSuggestion: "승인", + shiSuggestion: "승인", + }, +] + +const mockStandardAvlData: StandardAvlItem[] = [ + { + id: "s1", + no: 1, + designCategory: "케이블", + avlVendorName: "케이블업체A", + materialGroupInfo: "전기부품", + vendorInfo: "국내 케이블 제조사", + headquarterLocation: "한국", + tier: "Tier 1", + }, +] + +const mockVendorPoolData: VendorPoolItem[] = [ + { + id: "v1", + no: 1, + designCategory: "펌프", + avlVendorName: "펌프업체A", + materialGroupInfo: "기계부품", + vendorInfo: "국제 펌프 제조사", + vendorClassification: "주요업체", + faStatus: "완료", + recentQuoteNumber: "Q2024001", + recentOrderNumber: "PO2024001", + }, +] + +// 프로젝트 AVL 테이블 컬럼 +const projectAvlColumns: ColumnDef<ProjectAvlItem>[] = [ + { + 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: "designCategory", + header: "설계공종", + size: 120, + }, + { + accessorKey: "customerAvlMaterialName", + header: "고객사 AVL 자재명", + size: 150, + }, + { + accessorKey: "materialGroupInfo", + header: "자재그룹 정보", + size: 130, + }, + { + accessorKey: "avlVendorName", + header: "AVL 등재업체명", + size: 140, + }, + { + accessorKey: "vendorInfo", + header: "협력업체 정보", + size: 130, + }, + { + accessorKey: "ownerSuggestion", + header: "선주제안", + size: 100, + }, + { + accessorKey: "shiSuggestion", + header: "SHI 제안", + size: 100, + }, +] + +// 선종별 표준 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: "designCategory", + header: "설계공종", + size: 120, + }, + { + accessorKey: "avlVendorName", + header: "AVL 등재업체명", + size: 140, + }, + { + accessorKey: "materialGroupInfo", + header: "자재그룹 정보", + size: 130, + }, + { + accessorKey: "vendorInfo", + header: "협력업체 정보", + size: 130, + }, + { + accessorKey: "headquarterLocation", + header: "본사 위치 (국가)", + size: 140, + }, + { + accessorKey: "tier", + header: "등급 (Tier)", + size: 120, + }, +] + +// Vendor Pool 테이블 컬럼 +const vendorPoolColumns: ColumnDef<VendorPoolItem>[] = [ + { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row, table }) => { + // Vendor Pool 테이블의 단일 선택 핸들러 + 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: "designCategory", + header: "설계공종", + size: 120, + }, + { + accessorKey: "avlVendorName", + header: "AVL 등재업체명", + size: 140, + }, + { + accessorKey: "materialGroupInfo", + header: "자재그룹 정보", + size: 130, + }, + { + accessorKey: "vendorInfo", + header: "협력업체 정보", + size: 130, + }, + { + accessorKey: "vendorClassification", + header: "업체분류", + size: 100, + }, + { + accessorKey: "faStatus", + header: "FA현황", + size: 100, + }, + { + accessorKey: "recentQuoteNumber", + header: "최근견적번호", + size: 130, + }, + { + accessorKey: "recentOrderNumber", + header: "최근발주번호", + size: 130, + }, +] + +// 프로젝트 AVL 테이블 컴포넌트 +function ProjectAvlTable({ + onSelectionChange, + resetCounter +}: { + onSelectionChange?: (count: number) => void + resetCounter?: number +}) { + const [data] = React.useState(() => [...mockProjectAvlData]) + + const table = useReactTable({ + data, + columns: projectAvlColumns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + initialState: { + pagination: { + pageSize: 10, + }, + }, + }) + + // 선택 상태 변경 시 콜백 호출 + React.useEffect(() => { + const selectedRows = table.getFilteredSelectedRowModel().rows + onSelectionChange?.(selectedRows.length) + }, [table.getFilteredSelectedRowModel().rows.length, onSelectionChange]) + + // 선택 해제 요청이 오면 모든 선택 해제 + React.useEffect(() => { + if (resetCounter && resetCounter > 0) { + table.toggleAllPageRowsSelected(false) + } + }, [resetCounter, table]) + + return ( + <div className="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"> + 행 추가 + </Button> + <Button variant="outline" size="sm"> + 파일 업로드 + </Button> + <Button variant="outline" size="sm"> + 자동 매핑 + </Button> + <Button variant="outline" size="sm"> + 강제 매핑 + </Button> + <Button variant="outline" size="sm"> + 항목 삭제 + </Button> + </div> + </div> + </div> + <DataTable table={table} /> + </div> + ) +} + +// 선종별 표준 AVL 테이블 컴포넌트 +function StandardAvlTable({ + onSelectionChange, + resetCounter +}: { + onSelectionChange?: (count: number) => void + resetCounter?: number +}) { + const [data] = React.useState(() => [...mockStandardAvlData]) + + const table = useReactTable({ + data, + columns: standardAvlColumns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + initialState: { + pagination: { + pageSize: 10, + }, + }, + }) + + // 선택 상태 변경 시 콜백 호출 + 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="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"> + 신규업체 추가 + </Button> + <Button variant="outline" size="sm"> + 파일 업로드 + </Button> + <Button variant="outline" size="sm"> + 일괄입력 + </Button> + <Button variant="outline" size="sm"> + 항목삭제 + </Button> + </div> + </div> + </div> + <DataTable table={table} /> + </div> + ) +} + +// Vendor Pool 테이블 컴포넌트 +function VendorPoolTable({ + onSelectionChange, + resetCounter +}: { + onSelectionChange?: (count: number) => void + resetCounter?: number +}) { + const [data] = React.useState(() => [...mockVendorPoolData]) + + const table = useReactTable({ + data, + columns: vendorPoolColumns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + initialState: { + pagination: { + pageSize: 10, + }, + }, + }) + + // 선택 상태 변경 시 콜백 호출 + 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="flex flex-col"> + <div className="mb-2"> + <div className="flex items-center justify-between mb-2"> + <h4 className="font-medium">Vendor Pool</h4> + <div className="flex gap-1"> + <Button variant="outline" size="sm"> + 신규업체 추가 + </Button> + </div> + </div> + </div> + <DataTable table={table} /> + </div> + ) +} + +// 선택된 테이블 타입 +type SelectedTable = 'project' | 'standard' | 'vendor' | null + +// 선택 상태 액션 타입 +type SelectionAction = + | { type: 'SELECT_PROJECT'; count: number } + | { type: 'SELECT_STANDARD'; count: number } + | { type: 'SELECT_VENDOR'; count: number } + | { type: 'CLEAR_SELECTION' } + +// 선택 상태 +interface SelectionState { + selectedTable: SelectedTable + selectedRowCount: number + resetCounters: { + project: number + standard: number + vendor: number + } +} + +// 선택 상태 리듀서 +const selectionReducer = (state: SelectionState, action: SelectionAction): SelectionState => { + switch (action.type) { + case 'SELECT_PROJECT': + if (action.count > 0) { + return { + selectedTable: 'project', + selectedRowCount: action.count, + resetCounters: { + ...state.resetCounters, + standard: state.selectedTable !== 'project' ? state.resetCounters.standard + 1 : state.resetCounters.standard, + vendor: state.selectedTable !== 'project' ? state.resetCounters.vendor + 1 : state.resetCounters.vendor, + } + } + } else if (state.selectedTable === 'project') { + return { + ...state, + selectedTable: null, + selectedRowCount: 0, + } + } + return state + + case 'SELECT_STANDARD': + if (action.count > 0) { + return { + selectedTable: 'standard', + selectedRowCount: action.count, + resetCounters: { + ...state.resetCounters, + project: state.selectedTable !== 'standard' ? state.resetCounters.project + 1 : state.resetCounters.project, + vendor: state.selectedTable !== 'standard' ? state.resetCounters.vendor + 1 : state.resetCounters.vendor, + } + } + } else if (state.selectedTable === 'standard') { + return { + ...state, + selectedTable: null, + selectedRowCount: 0, + } + } + return state + + case 'SELECT_VENDOR': + if (action.count > 0) { + return { + selectedTable: 'vendor', + selectedRowCount: action.count, + resetCounters: { + ...state.resetCounters, + project: state.selectedTable !== 'vendor' ? state.resetCounters.project + 1 : state.resetCounters.project, + standard: state.selectedTable !== 'vendor' ? state.resetCounters.standard + 1 : state.resetCounters.standard, + } + } + } else if (state.selectedTable === 'vendor') { + return { + ...state, + selectedTable: null, + selectedRowCount: 0, + } + } + return state + + default: + return state + } +} + +// AVL 등록 영역 컴포넌트 +export function AvlRegistrationArea() { + // 단일 선택 상태 관리 (useReducer 사용) + const [selectionState, dispatch] = React.useReducer(selectionReducer, { + selectedTable: null, + selectedRowCount: 0, + resetCounters: { + project: 0, + standard: 0, + vendor: 0, + }, + }) + + // 선택 핸들러들 + const handleProjectSelection = React.useCallback((count: number) => { + dispatch({ type: 'SELECT_PROJECT', count }) + }, []) + + const handleStandardSelection = React.useCallback((count: number) => { + dispatch({ type: 'SELECT_STANDARD', count }) + }, []) + + const handleVendorSelection = React.useCallback((count: number) => { + dispatch({ type: 'SELECT_VENDOR', count }) + }, []) + + const { selectedTable, selectedRowCount, resetCounters } = selectionState + + return ( + <Card className="h-full min-w-full overflow-visible"> + {/* 고정 헤더 영역 */} + <div className="sticky top-0 z-10 p-4 border-b"> + <div className="flex items-center justify-between"> + <h3 className="text-lg font-semibold">AVL 등록</h3> + <div className="flex gap-2"> + <Button variant="outline" size="sm"> + 저장 + </Button> + <Button variant="outline" size="sm"> + 최종 확정 + </Button> + <Button variant="outline" size="sm"> + AVL 불러오기 + </Button> + </div> + </div> + </div> + + {/* 스크롤되는 콘텐츠 영역 */} + <div className="overflow-x-auto overflow-y-hidden"> + <div className="grid grid-cols-[2.2fr_2fr_2.5fr] gap-0 min-w-[1200px] w-fit"> + {/* 프로젝트 AVL 테이블 - 9개 컬럼 */} + <div className="p-4 border-r relative"> + <ProjectAvlTable + onSelectionChange={handleProjectSelection} + resetCounter={resetCounters.project} + /> + + {/* 이동 버튼들 - 첫 번째 border 위에 오버레이 */} + <div className="absolute right-0 top-1/2 transform -translate-y-1/2 translate-x-1/2 z-10"> + <div className="flex flex-col gap-2 bg-background border rounded-md p-1 shadow-sm"> + + <Button + variant="outline" + size="sm" + className="w-8 h-8 p-0" + title="왼쪽으로 이동" + disabled={selectedTable !== 'standard' || selectedRowCount === 0} + > + <ChevronLeft className="w-4 h-4" /> + </Button> + + <Button + variant="outline" + size="sm" + className="w-8 h-8 p-0" + title="오른쪽으로 이동" + disabled={selectedTable !== 'project' || selectedRowCount === 0} + > + <ChevronRight className="w-4 h-4" /> + </Button> + + <Button + variant="outline" + size="sm" + className="w-8 h-8 p-0" + title="오른쪽으로 이동" + disabled={selectedTable !== 'project' || selectedRowCount === 0} + > + <ChevronsRight className="w-4 h-4" /> + </Button> + + </div> + </div> + </div> + + {/* 선종별 표준 AVL 테이블 - 8개 컬럼 */} + <div className="p-4 border-r relative"> + <StandardAvlTable + onSelectionChange={handleStandardSelection} + resetCounter={resetCounters.standard} + /> + + {/* 이동 버튼들 - 두 번째 border 위에 오버레이 */} + <div className="absolute right-0 top-1/2 transform -translate-y-1/2 translate-x-1/2 z-10"> + <div className="flex flex-col gap-2 bg-background border rounded-md p-1 shadow-sm"> + <Button + variant="outline" + size="sm" + className="w-8 h-8 p-0" + title="왼쪽으로 이동" + disabled={selectedTable !== 'standard' || selectedRowCount === 0} + > + <ChevronLeft className="w-4 h-4" /> + </Button> + <Button + variant="outline" + size="sm" + className="w-8 h-8 p-0" + title="오른쪽으로 이동" + disabled={selectedTable !== 'standard' || selectedRowCount === 0} + > + <ChevronRight className="w-4 h-4" /> + </Button> + + </div> + </div> + </div> + + {/* Vendor Pool 테이블 - 10개 컬럼 */} + <div className="p-4 relative"> + <VendorPoolTable + onSelectionChange={handleVendorSelection} + resetCounter={resetCounters.vendor} + /> + + {/* 이동 버튼들 - 세 번째 테이블의 왼쪽 border 위에 오버레이 */} + <div className="absolute left-0 top-1/2 transform -translate-y-1/2 -translate-x-1/2 z-10"> + <div className="flex flex-col gap-2 bg-background border rounded-md p-1 shadow-sm"> + <Button + variant="outline" + size="sm" + className="w-8 h-8 p-0" + title="왼쪽으로 이동" + disabled={selectedTable !== 'vendor' || selectedRowCount === 0} + > + <ChevronsLeft className="w-4 h-4" /> + </Button> + <Button + variant="outline" + size="sm" + className="w-8 h-8 p-0" + title="왼쪽으로 이동" + disabled={selectedTable !== 'vendor' || selectedRowCount === 0} + > + <ChevronLeft className="w-4 h-4" /> + </Button> + <Button + variant="outline" + size="sm" + className="w-8 h-8 p-0" + title="오른쪽으로 이동" + disabled={selectedTable !== 'standard' || selectedRowCount === 0} + > + <ChevronRight className="w-4 h-4" /> + </Button> + </div> + </div> + </div> + </div> + </div> + </Card> + ) +} diff --git a/app/[lng]/evcp/(evcp)/avl/page.tsx b/app/[lng]/evcp/(evcp)/avl/page.tsx new file mode 100644 index 00000000..a5a5a170 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/avl/page.tsx @@ -0,0 +1,49 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { vendorPoSearchParamsCache } from "@/lib/po/vendor-table/validations" +import { getAvlLists } from "@/lib/avl/service" +import { AvlListItem } from "@/lib/avl/types" +import { AvlPageClient } from "./avl-page-client" + +interface AvlPageProps { + searchParams: Promise<SearchParams> +} + +// 서버에서 초기 데이터 로드 +async function getInitialAvlData(searchParams: SearchParams) { + try { + const search = vendorPoSearchParamsCache.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // 기본 파라미터로 전체 데이터 조회 + const result = await getAvlLists({ + page: 1, + perPage: 100, // 충분한 수량으로 조회 + sort: [{ id: "createdAt", desc: true }], + flags: [], + filters: validFilters, + joinOperator: "and", + search: search.search || "", + isTemplate: "" as any, + constructionSector: "", + projectCode: "", + shipType: "", + avlKind: "", + htDivision: "" as any, + rev: "", + }) + + return result.data + } catch (error) { + console.error("AVL 초기 데이터 로드 실패:", error) + return [] + } +} + +export default async function AvlPage(props: AvlPageProps) { + const searchParams = await props.searchParams + const initialData = await getInitialAvlData(searchParams) + + return <AvlPageClient initialData={initialData} /> +} |
