diff options
| author | joonhoekim <26rote@gmail.com> | 2025-09-22 12:17:48 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-09-22 12:17:48 +0900 |
| commit | bd3df05b4bdc07cef1bd79cf23c08a757e9ee6eb (patch) | |
| tree | aa726fc9dce49e3d346f0fdde282b6726ad1d815 | |
| parent | 087fc383a662d45a69b5971a6ad821209bcbaf5b (diff) | |
(김준회) AVL 수정요구 처리
- 제목추가
- 프로젝트선택기
| -rw-r--r-- | app/[lng]/evcp/(evcp)/avl/avl-page-client.tsx | 11 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/avl/page.tsx | 6 | ||||
| -rw-r--r-- | components/common/project/index.ts | 10 | ||||
| -rw-r--r-- | components/common/project/project-service.ts | 192 | ||||
| -rw-r--r-- | components/common/project/unified-project-selector.tsx | 318 | ||||
| -rw-r--r-- | lib/avl/table/project-avl-table.tsx | 154 |
6 files changed, 588 insertions, 103 deletions
diff --git a/app/[lng]/evcp/(evcp)/avl/avl-page-client.tsx b/app/[lng]/evcp/(evcp)/avl/avl-page-client.tsx index a9d5713d..f8df3c49 100644 --- a/app/[lng]/evcp/(evcp)/avl/avl-page-client.tsx +++ b/app/[lng]/evcp/(evcp)/avl/avl-page-client.tsx @@ -11,6 +11,7 @@ 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" +import { InformationButton } from "@/components/information/information-button" interface AvlPageClientProps { initialData: AvlListItem[] @@ -78,6 +79,16 @@ export function AvlPageClient({ initialData }: AvlPageClientProps) { return ( <div className="h-screen flex flex-col"> <div className="flex-1 overflow-hidden"> + + + {/* info button and header section */} + <div className="flex items-center gap-2 mt-2"> + <h2 className="text-2xl font-bold tracking-tight"> + AVL(Approved Vendor List) 목록 + </h2> + <InformationButton pagePath="evcp/avl" /> + </div> + <ResizablePanelGroup direction="vertical" className="h-full"> {/* 상단 패널: AVL 목록 */} <ResizablePanel defaultSize={40} minSize={20}> diff --git a/app/[lng]/evcp/(evcp)/avl/page.tsx b/app/[lng]/evcp/(evcp)/avl/page.tsx index 1c345cda..b1dbfeb0 100644 --- a/app/[lng]/evcp/(evcp)/avl/page.tsx +++ b/app/[lng]/evcp/(evcp)/avl/page.tsx @@ -3,7 +3,6 @@ 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 { @@ -45,5 +44,8 @@ export default async function AvlPage(props: AvlPageProps) { const searchParams = await props.searchParams const initialData = await getInitialAvlData(searchParams) - return <AvlPageClient initialData={initialData} /> + return (<> + <AvlPageClient initialData={initialData} /> + </> + ) } diff --git a/components/common/project/index.ts b/components/common/project/index.ts new file mode 100644 index 00000000..a5531f1b --- /dev/null +++ b/components/common/project/index.ts @@ -0,0 +1,10 @@ +// 공용 프로젝트 관련 컴포넌트 및 서비스 +export { UnifiedProjectSelector } from './unified-project-selector' +export type { UnifiedProjectSelectorProps } from './unified-project-selector' +export { + searchUnifiedProjects, + getProjectInfoByCode, + type UnifiedProject, + type ProjectSearchOptions, + type ProjectInfo +} from './project-service' diff --git a/components/common/project/project-service.ts b/components/common/project/project-service.ts new file mode 100644 index 00000000..6c103c6f --- /dev/null +++ b/components/common/project/project-service.ts @@ -0,0 +1,192 @@ +"use server" + +import db from "@/db/db" +import { projects, biddingProjects } from "@/db/schema" +import { eq, ilike, or } from "drizzle-orm" + +// 통합 프로젝트 타입 정의 +export interface UnifiedProject { + id: string // projects의 경우 id를 string으로, biddingProjects의 경우 pspid + code: string + name: string + type: string + source: 'projects' | 'biddingProjects' // 어느 테이블에서 왔는지 구분 + originalData?: any // 원본 데이터 (필요시 사용) +} + +// 프로젝트 검색 옵션 +export interface ProjectSearchOptions { + searchFrom: 'both' | 'projects' | 'biddingProjects' + searchTerm?: string + limit?: number +} + +// 프로젝트 정보 조회 결과 타입 +export interface ProjectInfo { + projectCode: string + projectName: string | null + shipType?: string | null + projectMsrm?: string | null + projectHtDivision?: string | null + source: 'projects' | 'biddingProjects' +} + +/** + * 통합 프로젝트 검색 - projects와 biddingProjects 테이블에서 검색 + */ +export async function searchUnifiedProjects(options: ProjectSearchOptions): Promise<{ + success: boolean + data: UnifiedProject[] + error?: string +}> { + try { + const { searchFrom, searchTerm, limit = 100 } = options + const results: UnifiedProject[] = [] + + // projects 테이블에서 검색 + if (searchFrom === 'both' || searchFrom === 'projects') { + try { + let projectsQuery = db.select({ + id: projects.id, + code: projects.code, + name: projects.name, + type: projects.type + }).from(projects) + + if (searchTerm && searchTerm.trim()) { + const term = `%${searchTerm.trim()}%` + projectsQuery = projectsQuery.where( + or( + ilike(projects.code, term), + ilike(projects.name, term) + ) + ) + } + + const projectsResults = await projectsQuery.limit(limit) + + for (const project of projectsResults) { + results.push({ + id: project.id.toString(), + code: project.code, + name: project.name, + type: project.type || 'project', + source: 'projects', + originalData: project + }) + } + } catch (error) { + console.error('Error searching projects table:', error) + // projects 테이블 검색 실패해도 계속 진행 + } + } + + // biddingProjects 테이블에서 검색 + if (searchFrom === 'both' || searchFrom === 'biddingProjects') { + try { + let biddingQuery = db.select({ + pspid: biddingProjects.pspid, + projNm: biddingProjects.projNm, + pjtType: biddingProjects.pjtType, + ptypeNm: biddingProjects.ptypeNm + }).from(biddingProjects) + + if (searchTerm && searchTerm.trim()) { + const term = `%${searchTerm.trim()}%` + biddingQuery = biddingQuery.where( + or( + ilike(biddingProjects.pspid, term), + ilike(biddingProjects.projNm, term) + ) + ) + } + + const biddingResults = await biddingQuery.limit(limit) + + for (const biddingProject of biddingResults) { + results.push({ + id: biddingProject.pspid, + code: biddingProject.pspid, + name: biddingProject.projNm || '', + type: biddingProject.pjtType || 'bidding', + source: 'biddingProjects', + originalData: biddingProject + }) + } + } catch (error) { + console.error('Error searching biddingProjects table:', error) + // biddingProjects 테이블 검색 실패해도 계속 진행 + } + } + + return { + success: true, + data: results + } + } catch (error) { + console.error('Error in searchUnifiedProjects:', error) + return { + success: false, + data: [], + error: '프로젝트 검색 중 오류가 발생했습니다.' + } + } +} + +/** + * 프로젝트 코드로 상세 정보 조회 + */ +export async function getProjectInfoByCode(projectCode: string, searchFrom: 'both' | 'projects' | 'biddingProjects' = 'both'): Promise<ProjectInfo | null> { + if (!projectCode.trim()) { + return null + } + + // projects 테이블에서 먼저 검색 + if (searchFrom === 'both' || searchFrom === 'projects') { + try { + const projectInfo = await db.select().from(projects).where(eq(projects.code, projectCode)).limit(1) + + if (projectInfo && projectInfo.length > 0) { + return { + projectCode: projectInfo[0].code, + projectName: projectInfo[0].name, + shipType: projectInfo[0].SKND || undefined, + projectHtDivision: projectInfo[0].type || undefined, + source: 'projects' + } + } + } catch (error) { + console.error('Error searching in projects table:', error) + } + } + + // biddingProjects 테이블에서 검색 + if (searchFrom === 'both' || searchFrom === 'biddingProjects') { + try { + const projectInfo = await db.select().from(biddingProjects).where(eq(biddingProjects.pspid, projectCode)).limit(1) + + if (projectInfo && projectInfo.length > 0) { + let projectHtDivision = null + if (projectInfo[0].pjtType === 'SHIP') { + projectHtDivision = 'H' + } else if (projectInfo[0].pjtType === 'HULL') { + projectHtDivision = 'H' + } else if (projectInfo[0].pjtType === 'TOP') { + projectHtDivision = 'T' + } + + return { + projectCode: projectInfo[0].pspid, + projectName: projectInfo[0].projNm, + projectMsrm: projectInfo[0].ptypeNm, + projectHtDivision, + source: 'biddingProjects' + } + } + } catch (error) { + console.error('Error searching in biddingProjects table:', error) + } + } + + return null +} diff --git a/components/common/project/unified-project-selector.tsx b/components/common/project/unified-project-selector.tsx new file mode 100644 index 00000000..b997ff2f --- /dev/null +++ b/components/common/project/unified-project-selector.tsx @@ -0,0 +1,318 @@ +'use client' + +import { useState, useEffect, useCallback, useMemo } from 'react' +import { Button } from '@/components/ui/button' +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' +import { Input } from '@/components/ui/input' +import { Badge } from '@/components/ui/badge' +import { Search, Check } from 'lucide-react' +import { + ColumnDef, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, + SortingState, + ColumnFiltersState, + VisibilityState, + RowSelectionState, +} from '@tanstack/react-table' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { searchUnifiedProjects, UnifiedProject, ProjectSearchOptions } from './project-service' +import { toast } from 'sonner' + +export interface UnifiedProjectSelectorProps { + selectedProject?: UnifiedProject + onProjectSelect: (project: UnifiedProject) => void + disabled?: boolean + searchOptions?: Partial<ProjectSearchOptions> + placeholder?: string + className?: string +} + +export function UnifiedProjectSelector({ + selectedProject, + onProjectSelect, + disabled, + searchOptions = {}, + placeholder = "프로젝트를 선택하세요", + className +}: UnifiedProjectSelectorProps) { + const [open, setOpen] = useState(false) + const [projects, setProjects] = useState<UnifiedProject[]>([]) + const [loading, setLoading] = useState(false) + const [sorting, setSorting] = useState<SortingState>([]) + const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]) + const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({}) + const [rowSelection, setRowSelection] = useState<RowSelectionState>({}) + const [globalFilter, setGlobalFilter] = useState('') + + // 기본 검색 옵션 설정 + const finalSearchOptions: ProjectSearchOptions = useMemo(() => ({ + searchFrom: 'both', + limit: 100, + ...searchOptions + }), [searchOptions]) + + // 테이블 컬럼 정의 + const columns: ColumnDef<UnifiedProject>[] = useMemo(() => [ + { + accessorKey: 'code', + header: '프로젝트 코드', + cell: ({ row }) => ( + <div className="font-mono text-sm">{row.getValue('code')}</div> + ), + }, + { + accessorKey: 'name', + header: '프로젝트명', + cell: ({ row }) => ( + <div className="max-w-[200px] truncate">{row.getValue('name')}</div> + ), + }, + { + accessorKey: 'type', + header: '타입', + cell: ({ row }) => ( + <Badge variant="outline">{row.getValue('type')}</Badge> + ), + }, + { + accessorKey: 'source', + header: '출처', + cell: ({ row }) => { + const source = row.getValue('source') as string + return ( + <Badge variant={source === 'projects' ? 'default' : 'secondary'}> + {source === 'projects' ? '프로젝트' : '견적프로젝트'} + </Badge> + ) + }, + }, + { + id: 'actions', + header: '선택', + cell: ({ row }) => ( + <Button + variant="ghost" + size="sm" + onClick={(e) => { + e.stopPropagation() + handleProjectSelect(row.original) + }} + > + <Check className="h-4 w-4" /> + </Button> + ), + }, + ], []) + + // 프로젝트 테이블 설정 + const table = useReactTable({ + data: projects, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + onGlobalFilterChange: setGlobalFilter, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + globalFilter, + }, + }) + + // 프로젝트 목록 로드 + const loadProjects = useCallback(async (searchTerm?: string) => { + setLoading(true) + try { + const result = await searchUnifiedProjects({ + ...finalSearchOptions, + searchTerm: searchTerm || globalFilter + }) + + if (result.success) { + setProjects(result.data) + } else { + toast.error(result.error || '프로젝트를 불러오는데 실패했습니다.') + setProjects([]) + } + } catch (error) { + console.error('프로젝트 목록 로드 실패:', error) + toast.error('프로젝트를 불러오는 중 오류가 발생했습니다.') + setProjects([]) + } finally { + setLoading(false) + } + }, [finalSearchOptions, globalFilter]) + + // 프로젝트 선택 핸들러 + const handleProjectSelect = useCallback((project: UnifiedProject) => { + onProjectSelect(project) + setOpen(false) + }, [onProjectSelect]) + + // 다이얼로그 열릴 때 프로젝트 목록 로드 + useEffect(() => { + if (open && projects.length === 0) { + loadProjects() + } + }, [open, projects.length, loadProjects]) + + // 검색어 변경 시 디바운스된 검색 + useEffect(() => { + if (!open) return + + const timeoutId = setTimeout(() => { + loadProjects(globalFilter) + }, 300) + + return () => clearTimeout(timeoutId) + }, [globalFilter, open, loadProjects]) + + return ( + <Dialog open={open} onOpenChange={setOpen}> + <DialogTrigger asChild> + <Button + variant="outline" + disabled={disabled} + className={`w-full justify-start ${className || ''}`} + > + {selectedProject ? ( + <div className="flex items-center gap-2 w-full"> + <span className="font-mono text-sm">[{selectedProject.code}]</span> + <span className="truncate flex-1 text-left">{selectedProject.name}</span> + <Badge variant={selectedProject.source === 'projects' ? 'default' : 'secondary'} className="text-xs"> + {selectedProject.source === 'projects' ? 'P' : 'B'} + </Badge> + </div> + ) : ( + <span className="text-muted-foreground">{placeholder}</span> + )} + </Button> + </DialogTrigger> + <DialogContent className="max-w-4xl max-h-[80vh]"> + <DialogHeader> + <DialogTitle>프로젝트 선택</DialogTitle> + <div className="text-sm text-muted-foreground"> + {finalSearchOptions.searchFrom === 'both' && '프로젝트 및 견적프로젝트에서 검색'} + {finalSearchOptions.searchFrom === 'projects' && '프로젝트에서 검색'} + {finalSearchOptions.searchFrom === 'biddingProjects' && '견적프로젝트에서 검색'} + </div> + </DialogHeader> + + <div className="space-y-4"> + <div className="flex items-center space-x-2"> + <Search className="h-4 w-4" /> + <Input + placeholder="프로젝트 코드, 이름으로 검색..." + value={globalFilter} + onChange={(e) => setGlobalFilter(e.target.value)} + className="flex-1" + /> + </div> + + {loading ? ( + <div className="flex justify-center py-8"> + <div className="text-sm text-muted-foreground">프로젝트를 불러오는 중...</div> + </div> + ) : ( + <div className="border rounded-md"> + <Table> + <TableHeader> + {table.getHeaderGroups().map((headerGroup) => ( + <TableRow key={headerGroup.id}> + {headerGroup.headers.map((header) => ( + <TableHead key={header.id}> + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + </TableHead> + ))} + </TableRow> + ))} + </TableHeader> + <TableBody> + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + <TableRow + key={row.id} + data-state={row.getIsSelected() && "selected"} + className="cursor-pointer hover:bg-muted/50" + onClick={() => handleProjectSelect(row.original)} + > + {row.getVisibleCells().map((cell) => ( + <TableCell key={cell.id}> + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + </TableCell> + ))} + </TableRow> + )) + ) : ( + <TableRow> + <TableCell + colSpan={columns.length} + className="h-24 text-center" + > + 검색 결과가 없습니다. + </TableCell> + </TableRow> + )} + </TableBody> + </Table> + </div> + )} + + <div className="flex items-center justify-between"> + <div className="text-sm text-muted-foreground"> + 총 {table.getFilteredRowModel().rows.length}개 프로젝트 + </div> + <div className="flex items-center space-x-2"> + <Button + variant="outline" + size="sm" + onClick={() => table.previousPage()} + disabled={!table.getCanPreviousPage()} + > + 이전 + </Button> + <div className="text-sm"> + {table.getState().pagination.pageIndex + 1} / {table.getPageCount()} + </div> + <Button + variant="outline" + size="sm" + onClick={() => table.nextPage()} + disabled={!table.getCanNextPage()} + > + 다음 + </Button> + </div> + </div> + </div> + </DialogContent> + </Dialog> + ) +} diff --git a/lib/avl/table/project-avl-table.tsx b/lib/avl/table/project-avl-table.tsx index 8664e32b..fc8f0f5e 100644 --- a/lib/avl/table/project-avl-table.tsx +++ b/lib/avl/table/project-avl-table.tsx @@ -1,14 +1,12 @@ "use client" import * as React from "react" -import { useReactTable, getCoreRowModel, getPaginationRowModel, getSortedRowModel, getFilteredRowModel } from "@tanstack/react-table" import { forwardRef, useImperativeHandle, useLayoutEffect, useMemo } from "react" import { DataTable } from "@/components/data-table/data-table" import { Button } from "@/components/ui/button" import { AvlVendorAddAndModifyDialog } from "./avl-vendor-add-and-modify-dialog" import { getProjectAvlVendorInfo, createAvlVendorInfo, updateAvlVendorInfo, deleteAvlVendorInfo, finalizeProjectAvl } from "../service" -import { getProjectInfoByProjectCode as getProjectInfoFromProjects } from "../../projects/service" -import { getProjectInfoByProjectCode as getProjectInfoFromBiddingProjects } from "../../bidding-projects/service" +import { useReactTable, getCoreRowModel, getPaginationRowModel, getSortedRowModel, getFilteredRowModel } from "@tanstack/react-table" import { GetProjectAvlSchema } from "../validations" import { AvlDetailItem, AvlVendorInfoInput } from "../types" import { toast } from "sonner" @@ -19,6 +17,7 @@ import { } from "../components/project-field-components" import { ProjectSearchStatus } from "../components/project-field-utils" import { useSession } from "next-auth/react" +import { UnifiedProjectSelector, UnifiedProject, getProjectInfoByCode } from "@/components/common/project" // 프로젝트 AVL 테이블에서는 AvlDetailItem을 사용 @@ -54,6 +53,9 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro const [pageCount, setPageCount] = React.useState(0) const [originalFile, setOriginalFile] = React.useState<string>("") const [localProjectCode, setLocalProjectCode] = React.useState<string>(projectCode || "") + + // 프로젝트 선택 상태 + const [selectedProject, setSelectedProject] = React.useState<UnifiedProject | undefined>(undefined) // 행 추가/수정 다이얼로그 상태 const [isAddDialogOpen, setIsAddDialogOpen] = React.useState(false) @@ -80,6 +82,7 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro }) + // 데이터 로드 함수 const loadData = React.useCallback(async (searchParams: Partial<GetProjectAvlSchema> = {}) => { try { @@ -163,47 +166,10 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro setProjectSearchStatus('searching') // 검색 시작 상태로 변경 try { - // 1. projects 테이블에서 먼저 검색 - let projectData: { - projectName?: string | null; - shipType?: string; - projectMsrm?: string | null; - projectHtDivision?: string | null; - } | null = null - let searchSource = 'projects' - - try { - projectData = await getProjectInfoFromProjects(projectCode.trim()) - // projects에서 찾았을 때만 즉시 성공 상태로 변경 - setProjectSearchStatus('success-projects') - } catch { - // projects 테이블에 없는 경우 biddingProjects 테이블에서 검색 - try { - projectData = await getProjectInfoFromBiddingProjects(projectCode.trim()) - if (projectData) { - searchSource = 'bidding-projects' - setProjectSearchStatus('success-bidding') // bidding에서 찾았을 때 성공 상태로 변경 - } else { - // 둘 다 실패한 경우에만 에러 상태로 변경 - setProjectInfo(null) - setProjectSearchStatus('error') - setData([]) - setPageCount(0) - toast.error("입력하신 프로젝트 코드를 찾을 수 없습니다.") - return - } - } catch { - // biddingProjects에서도 에러가 발생한 경우 - setProjectInfo(null) - setProjectSearchStatus('error') - setData([]) - setPageCount(0) - toast.error("입력하신 프로젝트 코드를 찾을 수 없습니다.") - return - } - } + const projectData = await getProjectInfoByCode(projectCode.trim(), 'both') if (projectData) { + // 프로젝트 정보 설정 setProjectInfo({ projectName: projectData.projectName || "", constructionSector: "조선", // 기본값으로 조선 설정 (필요시 로직 변경) @@ -211,11 +177,25 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro htDivision: projectData.projectHtDivision || "" }) - const sourceMessage = searchSource === 'projects' ? '프로젝트' : '견적프로젝트' + // 검색 상태 설정 + if (projectData.source === 'projects') { + setProjectSearchStatus('success-projects') + } else { + setProjectSearchStatus('success-bidding') + } + + const sourceMessage = projectData.source === 'projects' ? '프로젝트' : '견적프로젝트' toast.success(`${sourceMessage}에서 프로젝트 정보를 성공적으로 불러왔습니다.`) // 검색 성공 시 AVL 데이터 로드 트리거 setIsSearchClicked(true) + } else { + // 프로젝트를 찾을 수 없는 경우 + setProjectInfo(null) + setProjectSearchStatus('error') + setData([]) + setPageCount(0) + toast.error("입력하신 프로젝트 코드를 찾을 수 없습니다.") } } catch (error) { console.error("프로젝트 정보 조회 실패:", error) @@ -227,31 +207,20 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro } }, [setIsSearchClicked]) - // 프로젝트 코드 변경 핸들러 (입력만 처리) - const handleProjectCodeChange = React.useCallback((value: string) => { - setLocalProjectCode(value) - onProjectCodeChange?.(value) - // 입력이 변경되면 검색 상태를 idle로 초기화하고 검색 클릭 상태를 리셋 - if (!value.trim()) { - setProjectInfo(null) - setProjectSearchStatus('idle') - setIsSearchClicked(false) - setData([]) - setPageCount(0) - } else { - // 새로운 프로젝트 코드가 입력되면 검색 클릭 상태를 리셋 (다시 검색 버튼을 눌러야 함) - setIsSearchClicked(false) - } - }, [onProjectCodeChange]) - - // 프로젝트 검색 버튼 핸들러 - const handleProjectSearch = React.useCallback(async () => { + // 프로젝트 선택 핸들러 + const handleProjectSelect = React.useCallback(async (project: UnifiedProject) => { + setSelectedProject(project) + setLocalProjectCode(project.code) + onProjectCodeChange?.(project.code) + // 검색 시 페이지를 1페이지로 리셋 setPagination(prev => ({ ...prev, pageIndex: 0 })) - // 프로젝트 정보 검색 (성공 시 내부에서 AVL 데이터 로드 트리거) - await searchProject(localProjectCode) - }, [localProjectCode, searchProject]) + + // 선택된 프로젝트로 자동 검색 + await searchProject(project.code) + }, [onProjectCodeChange, searchProject]) + // 행 추가 핸들러 const handleAddRow = React.useCallback(() => { @@ -496,6 +465,7 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro } }, [resetCounter, table]) + return ( <div className="h-full flex flex-col"> <div className="mb-2"> @@ -542,43 +512,25 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro {/* 조회대상 관리영역 */} <div className="mb-4 p-4 border rounded-lg bg-muted/50"> <div className="flex gap-4 overflow-x-auto pb-2"> - {/* 프로젝트 코드 */} - <div className="flex flex-col gap-1 min-w-[200px]"> - <label className="text-sm font-medium">프로젝트 코드</label> - <div className="flex gap-2"> - <div className="flex-1"> - <input - type="text" - value={localProjectCode} - onChange={(e) => handleProjectCodeChange(e.target.value)} - placeholder="프로젝트 코드를 입력하세요" - className={`flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 ${ - projectSearchStatus === 'error' ? 'border-red-500' : - projectSearchStatus === 'success-projects' || projectSearchStatus === 'success-bidding' ? 'border-green-500' : - projectSearchStatus === 'searching' ? 'border-blue-500' : '' - }`} - disabled={projectSearchStatus === 'searching'} - /> - {projectSearchStatus !== 'idle' && ( - <div className="text-xs mt-1 text-muted-foreground"> - {projectSearchStatus === 'success-projects' ? '(프로젝트)' : - projectSearchStatus === 'success-bidding' ? '(견적프로젝트)' : - projectSearchStatus === 'searching' ? '(검색 중...)' : - projectSearchStatus === 'error' ? '(찾을 수 없음)' : - undefined} - </div> - )} + {/* 프로젝트 선택 */} + <div className="flex flex-col gap-1 min-w-[350px]"> + <label className="text-sm font-medium">프로젝트</label> + <UnifiedProjectSelector + selectedProject={selectedProject} + onProjectSelect={handleProjectSelect} + searchOptions={{ searchFrom: 'both' }} + placeholder="프로젝트를 선택하세요" + className="h-9" + /> + {projectSearchStatus !== 'idle' && ( + <div className="text-xs mt-1 text-muted-foreground"> + {projectSearchStatus === 'success-projects' ? '(프로젝트)' : + projectSearchStatus === 'success-bidding' ? '(견적프로젝트)' : + projectSearchStatus === 'searching' ? '(검색 중...)' : + projectSearchStatus === 'error' ? '(찾을 수 없음)' : + undefined} </div> - <Button - variant="outline" - size="sm" - onClick={handleProjectSearch} - disabled={!localProjectCode.trim() || projectSearchStatus === 'searching'} - className="px-3 h-9" - > - {projectSearchStatus === 'searching' ? '검색 중...' : '검색'} - </Button> - </div> + )} </div> {/* 프로젝트명 */} |
