summaryrefslogtreecommitdiff
path: root/lib/avl/table/project-avl-table.tsx
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-09-15 18:58:07 +0900
committerjoonhoekim <26rote@gmail.com>2025-09-15 18:58:07 +0900
commit2b490956c9752c1b756780a3461bc1c37b6fe0a7 (patch)
treeb0b8a03c8de5dfce4b6c7373a9d608306e9147c0 /lib/avl/table/project-avl-table.tsx
parente7818a457371849e29519497ebf046f385f05ab6 (diff)
(김준회) AVL 관리 및 상세 - 기능 구현 1차
+ docker compose 내 오류 수정
Diffstat (limited to 'lib/avl/table/project-avl-table.tsx')
-rw-r--r--lib/avl/table/project-avl-table.tsx720
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"