summaryrefslogtreecommitdiff
path: root/lib/avl/table/project-avl-table.tsx
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-09-15 01:23:00 +0000
committerjoonhoekim <26rote@gmail.com>2025-09-15 01:23:00 +0000
commite7818a457371849e29519497ebf046f385f05ab6 (patch)
tree9bf08ba1b31a512c481dc521c9dd7c90091a75b8 /lib/avl/table/project-avl-table.tsx
parent3f293c90beb58ce206a66ff444d7acfc41b56429 (diff)
(김준회) AVL 기능 구현 1차 및 벤더풀 E/B 구분 개선
Diffstat (limited to 'lib/avl/table/project-avl-table.tsx')
-rw-r--r--lib/avl/table/project-avl-table.tsx724
1 files changed, 724 insertions, 0 deletions
diff --git a/lib/avl/table/project-avl-table.tsx b/lib/avl/table/project-avl-table.tsx
new file mode 100644
index 00000000..c6dd8064
--- /dev/null
+++ b/lib/avl/table/project-avl-table.tsx
@@ -0,0 +1,724 @@
+"use client"
+
+import * as React from "react"
+import { useReactTable, getCoreRowModel, getPaginationRowModel, getSortedRowModel, getFilteredRowModel, type ColumnDef } from "@tanstack/react-table"
+import { DataTable } from "@/components/data-table/data-table"
+import { Button } from "@/components/ui/button"
+import { Checkbox } from "@/components/ui/checkbox"
+import { Input } from "@/components/ui/input"
+import { ProjectAvlAddDialog } from "./project-avl-add-dialog"
+import { getProjectAvlVendorInfo, getAvlListById, createAvlVendorInfo, updateAvlVendorInfo, deleteAvlVendorInfo } 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 { toast } from "sonner"
+
+// 프로젝트 AVL 테이블에서는 AvlDetailItem을 사용
+export type ProjectAvlItem = AvlDetailItem
+
+interface ProjectAvlTableProps {
+ onSelectionChange?: (count: number) => void
+ resetCounter?: number
+ projectCode?: string // 프로젝트 코드 필터
+ avlListId?: number // AVL 리스트 ID (관리 영역 표시용)
+ onProjectCodeChange?: (projectCode: string) => void // 프로젝트 코드 변경 콜백
+}
+
+// 프로젝트 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({
+ onSelectionChange,
+ resetCounter,
+ projectCode,
+ avlListId,
+ onProjectCodeChange
+}: ProjectAvlTableProps) {
+ 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 || "")
+
+ // 행 추가/수정 다이얼로그 상태
+ const [isAddDialogOpen, setIsAddDialogOpen] = React.useState(false)
+ const [editingItem, setEditingItem] = React.useState<AvlDetailItem | undefined>(undefined)
+
+ // 프로젝트 정보 상태
+ const [projectInfo, setProjectInfo] = React.useState<{
+ projectName: string
+ constructionSector: string
+ shipType: string
+ htDivision: string
+ } | null>(null)
+
+ // 프로젝트 검색 상태
+ const [projectSearchStatus, setProjectSearchStatus] = React.useState<'idle' | 'searching' | 'success-projects' | 'success-bidding' | 'error'>('idle')
+
+
+ // 데이터 로드 함수
+ const loadData = React.useCallback(async (searchParams: Partial<GetProjectAvlSchema>) => {
+ try {
+ setLoading(true)
+ const params: any = {
+ page: searchParams.page ?? 1,
+ perPage: searchParams.perPage ?? 10,
+ sort: searchParams.sort ?? [{ id: "no", desc: false }],
+ flags: searchParams.flags ?? [],
+ projectCode: localProjectCode || "",
+ equipBulkDivision: searchParams.equipBulkDivision ?? "",
+ disciplineCode: searchParams.disciplineCode ?? "",
+ disciplineName: searchParams.disciplineName ?? "",
+ materialNameCustomerSide: searchParams.materialNameCustomerSide ?? "",
+ packageCode: searchParams.packageCode ?? "",
+ packageName: searchParams.packageName ?? "",
+ materialGroupCode: searchParams.materialGroupCode ?? "",
+ materialGroupName: searchParams.materialGroupName ?? "",
+ vendorName: searchParams.vendorName ?? "",
+ vendorCode: searchParams.vendorCode ?? "",
+ avlVendorName: searchParams.avlVendorName ?? "",
+ tier: searchParams.tier ?? "",
+ filters: searchParams.filters ?? [],
+ joinOperator: searchParams.joinOperator ?? "and",
+ search: searchParams.search ?? "",
+ }
+ const result = await getProjectAvlVendorInfo(params)
+ setData(result.data)
+ setPageCount(result.pageCount)
+ } catch (error) {
+ console.error("프로젝트 AVL 데이터 로드 실패:", error)
+ setData([])
+ setPageCount(0)
+ } finally {
+ setLoading(false)
+ }
+ }, [localProjectCode])
+
+ // AVL 리스트 정보 로드
+ React.useEffect(() => {
+ const loadAvlListInfo = async () => {
+ if (avlListId) {
+ try {
+ const info = await getAvlListById(avlListId)
+ setAvlListInfo(info)
+ } catch (error) {
+ console.error("AVL 리스트 정보 로드 실패:", error)
+ }
+ }
+ }
+
+ loadAvlListInfo()
+ }, [avlListId])
+
+ // 초기 데이터 로드
+ React.useEffect(() => {
+ if (localProjectCode) {
+ loadData({})
+ }
+ }, [loadData, localProjectCode])
+
+ // 파일 업로드 핸들러
+ const handleFileUpload = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
+ const file = event.target.files?.[0]
+ if (file) {
+ setOriginalFile(file.name)
+ // TODO: 실제 파일 업로드 로직 구현
+ console.log("파일 업로드:", file.name)
+ }
+ }, [])
+
+ // 프로젝트 코드 변경 핸들러
+ const handleProjectCodeChange = React.useCallback(async (value: string) => {
+ setLocalProjectCode(value)
+ onProjectCodeChange?.(value)
+
+ // 프로젝트 코드가 입력된 경우 프로젝트 정보 조회
+ if (value.trim()) {
+ setProjectSearchStatus('searching') // 검색 시작 상태로 변경
+
+ try {
+ // 1. projects 테이블에서 먼저 검색
+ let projectData = null
+ let searchSource = 'projects'
+
+ 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에서도 에러가 발생한 경우
+ 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 || ""
+ })
+
+ const sourceMessage = searchSource === 'projects' ? '프로젝트' : '견적프로젝트'
+ toast.success(`${sourceMessage}에서 프로젝트 정보를 성공적으로 불러왔습니다.`)
+
+ // 프로젝트 검증 성공 시 해당 프로젝트의 AVL 데이터 로드
+ loadData({})
+ }
+ } catch (error) {
+ console.error("프로젝트 정보 조회 실패:", error)
+ setProjectInfo(null)
+ setProjectSearchStatus('error')
+ setData([])
+ setPageCount(0)
+ toast.error("프로젝트 정보를 불러오는데 실패했습니다.")
+ }
+ } else {
+ // 프로젝트 코드가 비어있는 경우 프로젝트 정보 초기화 및 데이터 클리어
+ setProjectInfo(null)
+ setProjectSearchStatus('idle')
+ setData([])
+ setPageCount(0)
+ }
+ }, [onProjectCodeChange])
+
+ // 행 추가 핸들러
+ const handleAddRow = React.useCallback(() => {
+ if (!localProjectCode.trim()) {
+ toast.error("프로젝트 코드를 먼저 입력해주세요.")
+ return
+ }
+ if (!projectInfo) {
+ toast.error("프로젝트 정보를 불러올 수 없습니다.")
+ return
+ }
+ setIsAddDialogOpen(true)
+ }, [localProjectCode, projectInfo])
+
+
+ // 다이얼로그에서 항목 추가 핸들러
+ const handleAddItem = React.useCallback(async (itemData: Omit<AvlVendorInfoInput, 'avlListId'>) => {
+ try {
+ // AVL 리스트 ID 확인
+ if (!avlListId) {
+ toast.error("AVL 리스트가 선택되지 않았습니다.")
+ return
+ }
+
+ // DB에 실제 저장할 데이터 준비
+ const saveData: AvlVendorInfoInput = {
+ ...itemData,
+ avlListId: avlListId // 현재 AVL 리스트 ID 설정
+ }
+
+ // DB에 저장
+ const result = await createAvlVendorInfo(saveData)
+
+ if (result) {
+ toast.success("새 항목이 성공적으로 추가되었습니다.")
+
+ // 데이터 새로고침
+ loadData({})
+ } else {
+ toast.error("항목 추가에 실패했습니다.")
+ }
+ } catch (error) {
+ console.error("항목 추가 실패:", error)
+ toast.error("항목 추가 중 오류가 발생했습니다.")
+ }
+ }, [avlListId, loadData])
+
+ // 다이얼로그에서 항목 수정 핸들러
+ const handleUpdateItem = React.useCallback(async (id: number, itemData: Omit<AvlVendorInfoInput, 'avlListId'>) => {
+ try {
+ // DB에 실제 수정
+ const result = await updateAvlVendorInfo(id, itemData)
+
+ if (result) {
+ toast.success("항목이 성공적으로 수정되었습니다.")
+
+ // 데이터 새로고침
+ loadData({})
+
+ // 다이얼로그 닫기 및 수정 모드 해제
+ setIsAddDialogOpen(false)
+ setEditingItem(undefined)
+ } else {
+ toast.error("항목 수정에 실패했습니다.")
+ }
+ } catch (error) {
+ console.error("항목 수정 실패:", error)
+ toast.error("항목 수정 중 오류가 발생했습니다.")
+ }
+ }, [loadData])
+
+ // 테이블 메타 설정
+ const tableMeta = React.useMemo(() => ({}), [])
+
+ const table = useReactTable({
+ data,
+ columns: getProjectAvlColumns(),
+ getCoreRowModel: getCoreRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ getFilteredRowModel: getFilteredRowModel(),
+ manualPagination: true,
+ pageCount,
+ initialState: {
+ pagination: {
+ pageSize: 10,
+ },
+ },
+ onPaginationChange: (updater) => {
+ const newState = typeof updater === 'function' ? updater(table.getState().pagination) : updater
+ loadData({
+ page: newState.pageIndex + 1,
+ perPage: newState.pageSize,
+ })
+ },
+ meta: tableMeta,
+ })
+
+ // 항목 수정 핸들러 (버튼 클릭)
+ const handleEditItem = React.useCallback(() => {
+ const selectedRows = table.getFilteredSelectedRowModel().rows
+
+ if (selectedRows.length !== 1) {
+ toast.error("수정할 항목을 하나만 선택해주세요.")
+ return
+ }
+
+ const selectedItem = selectedRows[0].original
+ setEditingItem(selectedItem)
+ setIsAddDialogOpen(true)
+ }, [table])
+
+ // 항목 삭제 핸들러
+ const handleDeleteItems = React.useCallback(async () => {
+ const selectedRows = table.getFilteredSelectedRowModel().rows
+
+ if (selectedRows.length === 0) {
+ toast.error("삭제할 항목을 선택해주세요.")
+ return
+ }
+
+ // 사용자 확인
+ const confirmed = window.confirm(`선택한 ${selectedRows.length}개 항목을 정말 삭제하시겠습니까?`)
+ if (!confirmed) return
+
+ try {
+ // 선택된 항목들을 DB에서 삭제
+ const deletePromises = selectedRows.map(async (row) => {
+ await deleteAvlVendorInfo(row.original.id)
+ })
+
+ await Promise.all(deletePromises)
+
+ toast.success(`${selectedRows.length}개 항목이 삭제되었습니다.`)
+
+ // 데이터 새로고침
+ loadData({})
+
+ // 선택 해제
+ table.toggleAllPageRowsSelected(false)
+ } catch (error) {
+ console.error("항목 삭제 실패:", error)
+ toast.error("항목 삭제 중 오류가 발생했습니다.")
+ }
+ }, [table, loadData])
+
+ // 선택 상태 변경 시 콜백 호출
+ 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="h-full flex flex-col">
+ <div className="mb-2">
+ <div className="flex items-center justify-between mb-2">
+ <h4 className="font-medium">프로젝트 AVL</h4>
+ <div className="flex gap-1">
+ <Button variant="outline" size="sm" onClick={handleAddRow}>
+ 행 추가
+ </Button>
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleEditItem}
+ disabled={table.getFilteredSelectedRowModel().rows.length !== 1}
+ >
+ 항목 수정
+ </Button>
+ <Button variant="outline" size="sm" onClick={() => toast.info("개발 중입니다.")}>
+ 파일 업로드
+ </Button>
+ <Button variant="outline" size="sm" onClick={() => toast.info("개발 중입니다.")}>
+ 자동 매핑
+ </Button>
+ <Button variant="outline" size="sm" onClick={() => toast.info("개발 중입니다.")}>
+ 강제 매핑
+ </Button>
+ <Button variant="outline" size="sm" onClick={handleDeleteItems}>
+ 항목 삭제
+ </Button>
+
+ {/* 최종 확정 버튼 */}
+ <Button variant="outline" size="sm" onClick={() => toast.info("개발 중입니다.")}>
+ 최종 확정
+ </Button>
+ </div>
+ </div>
+ </div>
+
+ {/* 조회대상 관리영역 */}
+ <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>
+
+ {/* 프로젝트명 */}
+ <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>
+
+ {/* 원본파일 */}
+ <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>
+
+ {/* 공사부문 */}
+ <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>
+
+ {/* 선종 */}
+ <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>
+
+ {/* 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>
+ </div>
+ </div>
+
+ <div className="flex-1">
+ <DataTable table={table} />
+ </div>
+
+ {/* 행 추가/수정 다이얼로그 */}
+ <ProjectAvlAddDialog
+ open={isAddDialogOpen}
+ onOpenChange={(open) => {
+ setIsAddDialogOpen(open)
+ if (!open) {
+ setEditingItem(undefined) // 다이얼로그가 닫힐 때 수정 모드 해제
+ }
+ }}
+ onAddItem={handleAddItem}
+ editingItem={editingItem}
+ onUpdateItem={handleUpdateItem}
+ />
+ </div>
+ )
+}