diff options
| author | joonhoekim <26rote@gmail.com> | 2025-09-15 18:58:07 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-09-15 18:58:07 +0900 |
| commit | 2b490956c9752c1b756780a3461bc1c37b6fe0a7 (patch) | |
| tree | b0b8a03c8de5dfce4b6c7373a9d608306e9147c0 /lib/avl/table/project-avl-table.tsx | |
| parent | e7818a457371849e29519497ebf046f385f05ab6 (diff) | |
(김준회) AVL 관리 및 상세 - 기능 구현 1차
+ docker compose 내 오류 수정
Diffstat (limited to 'lib/avl/table/project-avl-table.tsx')
| -rw-r--r-- | lib/avl/table/project-avl-table.tsx | 720 |
1 files changed, 323 insertions, 397 deletions
diff --git a/lib/avl/table/project-avl-table.tsx b/lib/avl/table/project-avl-table.tsx index c6dd8064..8664e32b 100644 --- a/lib/avl/table/project-avl-table.tsx +++ b/lib/avl/table/project-avl-table.tsx @@ -1,204 +1,57 @@ "use client" import * as React from "react" -import { useReactTable, getCoreRowModel, getPaginationRowModel, getSortedRowModel, getFilteredRowModel, type ColumnDef } from "@tanstack/react-table" +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 { Checkbox } from "@/components/ui/checkbox" -import { Input } from "@/components/ui/input" -import { ProjectAvlAddDialog } from "./project-avl-add-dialog" -import { getProjectAvlVendorInfo, getAvlListById, createAvlVendorInfo, updateAvlVendorInfo, deleteAvlVendorInfo } from "../service" +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 { GetProjectAvlSchema } from "../validations" -import { AvlDetailItem, AvlListItem, AvlVendorInfoInput } from "../types" +import { AvlDetailItem, AvlVendorInfoInput } from "../types" import { toast } from "sonner" +import { getProjectAvlColumns } from "./project-avl-table-columns" +import { + ProjectDisplayField, + ProjectFileField +} from "../components/project-field-components" +import { ProjectSearchStatus } from "../components/project-field-utils" +import { useSession } from "next-auth/react" + // 프로젝트 AVL 테이블에서는 AvlDetailItem을 사용 export type ProjectAvlItem = AvlDetailItem +// ref를 통해 외부에서 접근할 수 있는 메소드들 +export interface ProjectAvlTableRef { + getSelectedIds: () => number[] +} + interface ProjectAvlTableProps { onSelectionChange?: (count: number) => void resetCounter?: number projectCode?: string // 프로젝트 코드 필터 avlListId?: number // AVL 리스트 ID (관리 영역 표시용) onProjectCodeChange?: (projectCode: string) => void // 프로젝트 코드 변경 콜백 + reloadTrigger?: number } -// 프로젝트 AVL 테이블 컬럼 -const getProjectAvlColumns = (): 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, - cell: ({ row }) => { - return ( - <span> - {row.original.no} - </span> - ) - }, - }, - { - accessorKey: "disciplineName", - header: "설계공종", - size: 120, - cell: ({ row }) => { - return ( - <span> - {row.original.disciplineName} - </span> - ) - }, - }, - { - accessorKey: "materialNameCustomerSide", - header: "고객사 AVL 자재명", - size: 150, - cell: ({ row }) => { - return ( - <span> - {row.original.materialNameCustomerSide} - </span> - ) - }, - }, - { - accessorKey: "materialGroupCode", - header: "자재그룹 코드", - size: 120, - cell: ({ row }) => { - return ( - <span> - {row.original.materialGroupCode} - </span> - ) - }, - }, - { - accessorKey: "materialGroupName", - header: "자재그룹 명", - size: 130, - cell: ({ row }) => { - return ( - <span> - {row.original.materialGroupName} - </span> - ) - }, - }, - { - accessorKey: "avlVendorName", - header: "AVL 등재업체명", - size: 140, - cell: ({ row }) => { - return ( - <span> - {row.original.avlVendorName} - </span> - ) - }, - }, - { - accessorKey: "vendorCode", - header: "협력업체 코드", - size: 120, - cell: ({ row }) => { - return ( - <span> - {row.original.vendorCode} - </span> - ) - }, - }, - { - accessorKey: "vendorName", - header: "협력업체 명", - size: 130, - cell: ({ row }) => { - return ( - <span> - {row.original.vendorName} - </span> - ) - }, - }, - { - accessorKey: "ownerSuggestion", - header: "선주제안", - size: 100, - cell: ({ row }) => { - return ( - <span> - {row.original.ownerSuggestion ? "예" : "아니오"} - </span> - ) - }, - }, - { - accessorKey: "shiSuggestion", - header: "SHI 제안", - size: 100, - cell: ({ row }) => { - return ( - <span> - {row.original.shiSuggestion ? "예" : "아니오"} - </span> - ) - }, - }, -] -export function ProjectAvlTable({ +export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTableProps>(({ onSelectionChange, resetCounter, projectCode, avlListId, - onProjectCodeChange -}: ProjectAvlTableProps) { + onProjectCodeChange, + reloadTrigger +}, ref) => { + + const { data: sessionData } = useSession() + const [data, setData] = React.useState<ProjectAvlItem[]>([]) - const [loading, setLoading] = React.useState(false) const [pageCount, setPageCount] = React.useState(0) - const [avlListInfo, setAvlListInfo] = React.useState<AvlListItem | null>(null) const [originalFile, setOriginalFile] = React.useState<string>("") const [localProjectCode, setLocalProjectCode] = React.useState<string>(projectCode || "") @@ -215,20 +68,28 @@ export function ProjectAvlTable({ } | null>(null) // 프로젝트 검색 상태 - const [projectSearchStatus, setProjectSearchStatus] = React.useState<'idle' | 'searching' | 'success-projects' | 'success-bidding' | 'error'>('idle') + const [projectSearchStatus, setProjectSearchStatus] = React.useState<ProjectSearchStatus>('idle') + + // 검색 버튼 클릭 여부 상태 + const [isSearchClicked, setIsSearchClicked] = React.useState(false) + + // 페이지네이션 상태 + const [pagination, setPagination] = React.useState({ + pageIndex: 0, + pageSize: 10, + }) // 데이터 로드 함수 - const loadData = React.useCallback(async (searchParams: Partial<GetProjectAvlSchema>) => { + const loadData = React.useCallback(async (searchParams: Partial<GetProjectAvlSchema> = {}) => { try { - setLoading(true) - const params: any = { + const params = { page: searchParams.page ?? 1, perPage: searchParams.perPage ?? 10, sort: searchParams.sort ?? [{ id: "no", desc: false }], flags: searchParams.flags ?? [], projectCode: localProjectCode || "", - equipBulkDivision: searchParams.equipBulkDivision ?? "", + equipBulkDivision: (searchParams.equipBulkDivision as "EQUIP" | "BULK") ?? "EQUIP", disciplineCode: searchParams.disciplineCode ?? "", disciplineName: searchParams.disciplineName ?? "", materialNameCustomerSide: searchParams.materialNameCustomerSide ?? "", @@ -244,7 +105,13 @@ export function ProjectAvlTable({ joinOperator: searchParams.joinOperator ?? "and", search: searchParams.search ?? "", } + console.log('ProjectAvlTable - API call params:', params) const result = await getProjectAvlVendorInfo(params) + console.log('ProjectAvlTable - API result:', { + dataCount: result.data.length, + pageCount: result.pageCount, + requestedPage: params.page + }) setData(result.data) setPageCount(result.pageCount) } catch (error) { @@ -252,32 +119,26 @@ export function ProjectAvlTable({ setData([]) setPageCount(0) } finally { - setLoading(false) + // 로딩 상태 처리 완료 } }, [localProjectCode]) - // AVL 리스트 정보 로드 + + + // reloadTrigger가 변경될 때마다 데이터 리로드 React.useEffect(() => { - const loadAvlListInfo = async () => { - if (avlListId) { - try { - const info = await getAvlListById(avlListId) - setAvlListInfo(info) - } catch (error) { - console.error("AVL 리스트 정보 로드 실패:", error) - } - } + if (reloadTrigger && reloadTrigger > 0) { + console.log('ProjectAvlTable - reloadTrigger changed, reloading data') + loadData({}) } + }, [reloadTrigger, loadData]) - loadAvlListInfo() - }, [avlListId]) - - // 초기 데이터 로드 + // 초기 데이터 로드 (검색 버튼이 눌렸을 때만) React.useEffect(() => { - if (localProjectCode) { + if (localProjectCode && isSearchClicked) { loadData({}) } - }, [loadData, localProjectCode]) + }, [loadData, localProjectCode, isSearchClicked]) // 파일 업로드 핸들러 const handleFileUpload = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => { @@ -289,42 +150,41 @@ export function ProjectAvlTable({ } }, []) - // 프로젝트 코드 변경 핸들러 - const handleProjectCodeChange = React.useCallback(async (value: string) => { - setLocalProjectCode(value) - onProjectCodeChange?.(value) + // 프로젝트 검색 함수 (공통 로직) + const searchProject = React.useCallback(async (projectCode: string) => { + if (!projectCode.trim()) { + setProjectInfo(null) + setProjectSearchStatus('idle') + setData([]) + setPageCount(0) + return + } - // 프로젝트 코드가 입력된 경우 프로젝트 정보 조회 - if (value.trim()) { - setProjectSearchStatus('searching') // 검색 시작 상태로 변경 + setProjectSearchStatus('searching') // 검색 시작 상태로 변경 - try { - // 1. projects 테이블에서 먼저 검색 - let projectData = null - let searchSource = 'projects' + 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 getProjectInfoFromProjects(value.trim()) - // projects에서 찾았을 때만 즉시 성공 상태로 변경 - setProjectSearchStatus('success-projects') - } catch (projectsError) { - // projects 테이블에 없는 경우 biddingProjects 테이블에서 검색 - try { - projectData = await getProjectInfoFromBiddingProjects(value.trim()) - if (projectData) { - searchSource = 'bidding-projects' - setProjectSearchStatus('success-bidding') // bidding에서 찾았을 때 성공 상태로 변경 - } else { - // 둘 다 실패한 경우에만 에러 상태로 변경 - setProjectInfo(null) - setProjectSearchStatus('error') - setData([]) - setPageCount(0) - toast.error("입력하신 프로젝트 코드를 찾을 수 없습니다.") - return - } - } catch (biddingError) { - // biddingProjects에서도 에러가 발생한 경우 + projectData = await getProjectInfoFromBiddingProjects(projectCode.trim()) + if (projectData) { + searchSource = 'bidding-projects' + setProjectSearchStatus('success-bidding') // bidding에서 찾았을 때 성공 상태로 변경 + } else { + // 둘 다 실패한 경우에만 에러 상태로 변경 setProjectInfo(null) setProjectSearchStatus('error') setData([]) @@ -332,39 +192,67 @@ export function ProjectAvlTable({ toast.error("입력하신 프로젝트 코드를 찾을 수 없습니다.") return } + } catch { + // biddingProjects에서도 에러가 발생한 경우 + setProjectInfo(null) + setProjectSearchStatus('error') + setData([]) + setPageCount(0) + toast.error("입력하신 프로젝트 코드를 찾을 수 없습니다.") + return } + } - if (projectData) { - setProjectInfo({ - projectName: projectData.projectName || "", - constructionSector: "조선", // 기본값으로 조선 설정 (필요시 로직 변경) - shipType: projectData.shipType || projectData.projectMsrm || "", - htDivision: projectData.projectHtDivision || "" - }) + if (projectData) { + setProjectInfo({ + projectName: projectData.projectName || "", + constructionSector: "조선", // 기본값으로 조선 설정 (필요시 로직 변경) + shipType: projectData.shipType || projectData.projectMsrm || "", + htDivision: projectData.projectHtDivision || "" + }) - const sourceMessage = searchSource === 'projects' ? '프로젝트' : '견적프로젝트' - toast.success(`${sourceMessage}에서 프로젝트 정보를 성공적으로 불러왔습니다.`) + const sourceMessage = searchSource === 'projects' ? '프로젝트' : '견적프로젝트' + toast.success(`${sourceMessage}에서 프로젝트 정보를 성공적으로 불러왔습니다.`) - // 프로젝트 검증 성공 시 해당 프로젝트의 AVL 데이터 로드 - loadData({}) - } - } catch (error) { - console.error("프로젝트 정보 조회 실패:", error) - setProjectInfo(null) - setProjectSearchStatus('error') - setData([]) - setPageCount(0) - toast.error("프로젝트 정보를 불러오는데 실패했습니다.") + // 검색 성공 시 AVL 데이터 로드 트리거 + setIsSearchClicked(true) } - } else { - // 프로젝트 코드가 비어있는 경우 프로젝트 정보 초기화 및 데이터 클리어 + } catch (error) { + console.error("프로젝트 정보 조회 실패:", error) + setProjectInfo(null) + setProjectSearchStatus('error') + setData([]) + setPageCount(0) + toast.error("프로젝트 정보를 불러오는데 실패했습니다.") + } + }, [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 () => { + // 검색 시 페이지를 1페이지로 리셋 + setPagination(prev => ({ ...prev, pageIndex: 0 })) + // 프로젝트 정보 검색 (성공 시 내부에서 AVL 데이터 로드 트리거) + await searchProject(localProjectCode) + }, [localProjectCode, searchProject]) + // 행 추가 핸들러 const handleAddRow = React.useCallback(() => { if (!localProjectCode.trim()) { @@ -382,16 +270,11 @@ export function ProjectAvlTable({ // 다이얼로그에서 항목 추가 핸들러 const handleAddItem = React.useCallback(async (itemData: Omit<AvlVendorInfoInput, 'avlListId'>) => { try { - // AVL 리스트 ID 확인 - if (!avlListId) { - toast.error("AVL 리스트가 선택되지 않았습니다.") - return - } - - // DB에 실제 저장할 데이터 준비 + // DB에 실제 저장할 데이터 준비 (avlListId는 나중에 최종 확정 시 설정) const saveData: AvlVendorInfoInput = { ...itemData, - avlListId: avlListId // 현재 AVL 리스트 ID 설정 + projectCode: localProjectCode, // 현재 프로젝트 코드 저장 + avlListId: avlListId || undefined // 있으면 설정, 없으면 undefined (null로 저장됨) } // DB에 저장 @@ -409,7 +292,7 @@ export function ProjectAvlTable({ console.error("항목 추가 실패:", error) toast.error("항목 추가 중 오류가 발생했습니다.") } - }, [avlListId, loadData]) + }, [avlListId, loadData, localProjectCode]) // 다이얼로그에서 항목 수정 핸들러 const handleUpdateItem = React.useCallback(async (id: number, itemData: Omit<AvlVendorInfoInput, 'avlListId'>) => { @@ -447,21 +330,43 @@ export function ProjectAvlTable({ getFilteredRowModel: getFilteredRowModel(), manualPagination: true, pageCount, - initialState: { - pagination: { - pageSize: 10, - }, + state: { + pagination, }, onPaginationChange: (updater) => { - const newState = typeof updater === 'function' ? updater(table.getState().pagination) : updater - loadData({ - page: newState.pageIndex + 1, - perPage: newState.pageSize, + // 페이지네이션 상태 업데이트 + const newPaginationState = typeof updater === 'function' ? updater(pagination) : updater + + console.log('ProjectAvlTable - Pagination changed:', { + currentState: pagination, + newPaginationState, + localProjectCode, + isSearchClicked, + willLoadData: localProjectCode && isSearchClicked }) + + setPagination(newPaginationState) + + if (localProjectCode && isSearchClicked) { + const apiParams = { + page: newPaginationState.pageIndex + 1, + perPage: newPaginationState.pageSize, + } + console.log('ProjectAvlTable - Loading data with params:', apiParams) + loadData(apiParams) + } }, meta: tableMeta, }) + // 외부에서 선택된 ID들을 가져올 수 있도록 ref에 메소드 노출 + useImperativeHandle(ref, () => ({ + getSelectedIds: () => { + const selectedRows = table.getFilteredSelectedRowModel().rows + return selectedRows.map(row => row.original.id) + } + })) + // 항목 수정 핸들러 (버튼 클릭) const handleEditItem = React.useCallback(() => { const selectedRows = table.getFilteredSelectedRowModel().rows @@ -510,11 +415,79 @@ export function ProjectAvlTable({ } }, [table, loadData]) + // 최종 확정 핸들러 + const handleFinalizeAvl = React.useCallback(async () => { + // 1. 필수 조건 검증 + if (!localProjectCode.trim()) { + toast.error("프로젝트 코드를 먼저 입력해주세요.") + return + } + + if (!projectInfo) { + toast.error("프로젝트 정보를 불러올 수 없습니다. 프로젝트 코드를 다시 확인해주세요.") + return + } + + if (data.length === 0) { + toast.error("확정할 AVL 벤더 정보가 없습니다.") + return + } + + // 2. 사용자 확인 + const confirmed = window.confirm( + `현재 프로젝트(${localProjectCode})의 AVL을 최종 확정하시겠습니까?\n\n` + + `- 프로젝트명: ${projectInfo.projectName}\n` + + `- 벤더 정보: ${data.length}개\n` + + `- 공사부문: ${projectInfo.constructionSector}\n` + + `- 선종: ${projectInfo.shipType}\n` + + `- H/T 구분: ${projectInfo.htDivision}\n\n` + + `확정 후에는 수정이 어려울 수 있습니다.` + ) + + if (!confirmed) return + + try { + // 3. 현재 데이터의 모든 ID 수집 + const avlVendorInfoIds = data.map(item => item.id) + + // 4. 최종 확정 실행 + const result = await finalizeProjectAvl( + localProjectCode, + projectInfo, + avlVendorInfoIds, + sessionData?.user?.name || "" + ) + + if (result.success) { + toast.success(result.message) + + // 5. 데이터 새로고침 + loadData({}) + + // 6. 선택 해제 + table.toggleAllPageRowsSelected(false) + } else { + toast.error(result.message) + } + } catch (error) { + console.error("AVL 최종 확정 실패:", error) + toast.error("AVL 최종 확정 중 오류가 발생했습니다.") + } + }, [localProjectCode, projectInfo, data, table, loadData, sessionData?.user?.name]) + + // 선택된 행 개수 (안정적인 계산을 위해 useMemo 사용) + const selectedRows = table.getFilteredSelectedRowModel().rows + const selectedRowCount = useMemo(() => { + const count = selectedRows.length + console.log('ProjectAvlTable - selectedRowCount calculated:', count) + return count + }, [selectedRows]) + // 선택 상태 변경 시 콜백 호출 - React.useEffect(() => { - const selectedRows = table.getFilteredSelectedRowModel().rows - onSelectionChange?.(selectedRows.length) - }, [table.getFilteredSelectedRowModel().rows.length, onSelectionChange]) + useLayoutEffect(() => { + console.log('ProjectAvlTable - onSelectionChange called with count:', selectedRowCount) + onSelectionChange?.(selectedRowCount) + }, [selectedRowCount, onSelectionChange]) // 선택 해제 요청이 오면 모든 선택 해제 React.useEffect(() => { @@ -540,7 +513,7 @@ export function ProjectAvlTable({ > 항목 수정 </Button> - <Button variant="outline" size="sm" onClick={() => toast.info("개발 중입니다.")}> + {/* <Button variant="outline" size="sm" onClick={() => toast.info("개발 중입니다.")}> 파일 업로드 </Button> <Button variant="outline" size="sm" onClick={() => toast.info("개발 중입니다.")}> @@ -548,13 +521,18 @@ export function ProjectAvlTable({ </Button> <Button variant="outline" size="sm" onClick={() => toast.info("개발 중입니다.")}> 강제 매핑 - </Button> + </Button> */} <Button variant="outline" size="sm" onClick={handleDeleteItems}> 항목 삭제 </Button> {/* 최종 확정 버튼 */} - <Button variant="outline" size="sm" onClick={() => toast.info("개발 중입니다.")}> + <Button + variant="outline" + size="sm" + onClick={handleFinalizeAvl} + disabled={!localProjectCode.trim() || !projectInfo || data.length === 0} + > 최종 확정 </Button> </div> @@ -565,140 +543,84 @@ export function ProjectAvlTable({ <div className="mb-4 p-4 border rounded-lg bg-muted/50"> <div className="flex gap-4 overflow-x-auto pb-2"> {/* 프로젝트 코드 */} - <div className="space-y-2 min-w-[250px] flex-shrink-0"> - <label className={`text-sm font-medium ${ - projectSearchStatus === 'error' ? 'text-red-600' : - projectSearchStatus === 'success-projects' || projectSearchStatus === 'success-bidding' ? 'text-green-600' : - projectSearchStatus === 'searching' ? 'text-blue-600' : - 'text-muted-foreground' - }`}> - 프로젝트 코드 - {projectSearchStatus === 'success-projects' && <span className="ml-1 text-xs">(프로젝트)</span>} - {projectSearchStatus === 'success-bidding' && <span className="ml-1 text-xs">(견적프로젝트)</span>} - {projectSearchStatus === 'searching' && <span className="ml-1 text-xs">(검색 중...)</span>} - {projectSearchStatus === 'error' && <span className="ml-1 text-xs">(찾을 수 없음)</span>} - </label> - <Input - value={localProjectCode} - onChange={(e) => handleProjectCodeChange(e.target.value)} - placeholder="프로젝트 코드를 입력하세요" - // disabled={projectSearchStatus === 'searching'} - className={`h-8 text-sm ${ - projectSearchStatus === 'error' ? 'border-red-300 focus:border-red-500 focus:ring-red-500/20' : - projectSearchStatus === 'success-projects' || projectSearchStatus === 'success-bidding' ? 'border-green-300 focus:border-green-500 focus:ring-green-500/20' : - projectSearchStatus === 'searching' ? 'border-blue-300 focus:border-blue-500 focus:ring-blue-500/20' : - '' - }`} - /> + <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> + <Button + variant="outline" + size="sm" + onClick={handleProjectSearch} + disabled={!localProjectCode.trim() || projectSearchStatus === 'searching'} + className="px-3 h-9" + > + {projectSearchStatus === 'searching' ? '검색 중...' : '검색'} + </Button> + </div> </div> {/* 프로젝트명 */} - <div className="space-y-2 min-w-[250px] flex-shrink-0"> - <label className={`text-sm font-medium ${ - projectSearchStatus === 'error' ? 'text-red-600' : - projectSearchStatus === 'success-projects' || projectSearchStatus === 'success-bidding' ? 'text-green-600' : - projectSearchStatus === 'searching' ? 'text-blue-600' : - 'text-muted-foreground' - }`}> - 프로젝트명 - {projectSearchStatus === 'searching' && <span className="ml-1 text-xs">(조회 중...)</span>} - </label> - <div className={`text-sm font-medium min-h-[32px] flex items-center border rounded-md px-3 bg-background ${ - projectSearchStatus === 'error' ? 'border-red-300' : - projectSearchStatus === 'success-projects' || projectSearchStatus === 'success-bidding' ? 'border-green-300' : - projectSearchStatus === 'searching' ? 'border-blue-300' : - 'border-input' - }`}> - {projectSearchStatus === 'searching' ? '조회 중...' : (projectInfo?.projectName || '-')} - </div> - </div> + <ProjectDisplayField + label="프로젝트명" + value={projectInfo?.projectName || ''} + status={projectSearchStatus} + minWidth="250px" + /> {/* 원본파일 */} - <div className="space-y-2 min-w-[200px] flex-shrink-0"> - <label className="text-sm font-medium text-muted-foreground">원본파일</label> - <div className="flex items-center gap-2 min-h-[32px]"> - {originalFile ? ( - <span className="text-sm text-blue-600">{originalFile}</span> - ) : ( - <div className="relative"> - <input - type="file" - onChange={handleFileUpload} - className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" - accept=".xlsx,.xls,.csv" - /> - <Button variant="outline" size="sm" className="text-xs"> - 파일 선택 - </Button> - </div> - )} - </div> - </div> + <ProjectFileField + label="원본파일" + originalFile={originalFile} + onFileUpload={handleFileUpload} + /> {/* 공사부문 */} - <div className="space-y-2 min-w-[120px] flex-shrink-0"> - <label className={`text-sm font-medium ${ - projectSearchStatus === 'error' ? 'text-red-600' : - projectSearchStatus === 'success-projects' || projectSearchStatus === 'success-bidding' ? 'text-green-600' : - projectSearchStatus === 'searching' ? 'text-blue-600' : - 'text-muted-foreground' - }`}> - 공사부문 - {projectSearchStatus === 'searching' && <span className="ml-1 text-xs">(조회 중...)</span>} - </label> - <div className={`text-sm font-medium min-h-[32px] flex items-center border rounded-md px-3 bg-background ${ - projectSearchStatus === 'error' ? 'border-red-300' : - projectSearchStatus === 'success-projects' || projectSearchStatus === 'success-bidding' ? 'border-green-300' : - projectSearchStatus === 'searching' ? 'border-blue-300' : - 'border-input' - }`}> - {projectSearchStatus === 'searching' ? '조회 중...' : (projectInfo?.constructionSector || '-')} - </div> - </div> + <ProjectDisplayField + label="공사부문" + value={projectInfo?.constructionSector || ''} + status={projectSearchStatus} + /> {/* 선종 */} - <div className="space-y-2 min-w-[120px] flex-shrink-0"> - <label className={`text-sm font-medium ${ - projectSearchStatus === 'error' ? 'text-red-600' : - projectSearchStatus === 'success-projects' || projectSearchStatus === 'success-bidding' ? 'text-green-600' : - projectSearchStatus === 'searching' ? 'text-blue-600' : - 'text-muted-foreground' - }`}> - 선종 - {projectSearchStatus === 'searching' && <span className="ml-1 text-xs">(조회 중...)</span>} - </label> - <div className={`text-sm font-medium min-h-[32px] flex items-center border rounded-md px-3 bg-background ${ - projectSearchStatus === 'error' ? 'border-red-300' : - projectSearchStatus === 'success-projects' || projectSearchStatus === 'success-bidding' ? 'border-green-300' : - projectSearchStatus === 'searching' ? 'border-blue-300' : - 'border-input' - }`}> - {projectSearchStatus === 'searching' ? '조회 중...' : (projectInfo?.shipType || '-')} - </div> - </div> + <ProjectDisplayField + label="선종" + value={projectInfo?.shipType || ''} + status={projectSearchStatus} + /> {/* H/T 구분 */} - <div className="space-y-2 min-w-[140px] flex-shrink-0"> - <label className={`text-sm font-medium ${ - projectSearchStatus === 'error' ? 'text-red-600' : - projectSearchStatus === 'success-projects' || projectSearchStatus === 'success-bidding' ? 'text-green-600' : - projectSearchStatus === 'searching' ? 'text-blue-600' : - 'text-muted-foreground' - }`}> - H/T 구분 - {projectSearchStatus === 'searching' && <span className="ml-1 text-xs">(조회 중...)</span>} - </label> - <div className={`text-sm font-medium min-h-[32px] flex items-center border rounded-md px-3 bg-background ${ - projectSearchStatus === 'error' ? 'border-red-300' : - projectSearchStatus === 'success-projects' || projectSearchStatus === 'success-bidding' ? 'border-green-300' : - projectSearchStatus === 'searching' ? 'border-blue-300' : - 'border-input' - }`}> - {projectSearchStatus === 'searching' ? '조회 중...' : - (projectInfo?.htDivision === 'H' ? 'Hull (H)' : - projectInfo?.htDivision === 'T' ? 'Topside (T)' : '-')} - </div> - </div> + <ProjectDisplayField + label="H/T 구분" + value={projectInfo?.htDivision || ''} + status={projectSearchStatus} + minWidth="140px" + formatter={(value) => + value === 'H' ? 'Hull (H)' : + value === 'T' ? 'Topside (T)' : '-' + } + /> </div> </div> @@ -707,7 +629,7 @@ export function ProjectAvlTable({ </div> {/* 행 추가/수정 다이얼로그 */} - <ProjectAvlAddDialog + <AvlVendorAddAndModifyDialog open={isAddDialogOpen} onOpenChange={(open) => { setIsAddDialogOpen(open) @@ -718,7 +640,11 @@ export function ProjectAvlTable({ onAddItem={handleAddItem} editingItem={editingItem} onUpdateItem={handleUpdateItem} + isTemplate={false} // 프로젝트 AVL 모드 + initialProjectCode={localProjectCode} /> </div> ) -} +}) + +ProjectAvlTable.displayName = "ProjectAvlTable" |
