From ba8cd44a0ed2c613a5f2cee06bfc9bd0f61f21c7 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Fri, 7 Nov 2025 08:39:04 +0000
Subject: (최겸) 입찰/견적 수정사항
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../selectors/cost-center/cost-center-selector.tsx | 335 +++++++++++++++++++++
 1 file changed, 335 insertions(+)
 create mode 100644 components/common/selectors/cost-center/cost-center-selector.tsx

(limited to 'components/common/selectors/cost-center/cost-center-selector.tsx')

diff --git a/components/common/selectors/cost-center/cost-center-selector.tsx b/components/common/selectors/cost-center/cost-center-selector.tsx
new file mode 100644
index 00000000..32c37973
--- /dev/null
+++ b/components/common/selectors/cost-center/cost-center-selector.tsx
@@ -0,0 +1,335 @@
+'use client'
+
+/**
+ * Cost Center 선택기
+ *
+ * @description
+ * - 오라클에서 Cost Center들을 조회
+ * - KOSTL: Cost Center 코드
+ * - KTEXT: 단축명
+ * - LTEXT: 설명
+ * - DATAB: 시작일
+ * - DATBI: 종료일
+ */
+
+import { useState, useCallback, useMemo, useTransition } 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 { 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 {
+  getCostCenters,
+  CostCenter
+} from './cost-center-service'
+import { toast } from 'sonner'
+
+export interface CostCenterSelectorProps {
+  selectedCode?: CostCenter
+  onCodeSelect: (code: CostCenter) => void
+  disabled?: boolean
+  placeholder?: string
+  className?: string
+}
+
+export interface CostCenterItem {
+  kostl: string // Cost Center
+  ktext: string // 단축명
+  ltext: string // 설명
+  datab: string // 시작일
+  datbi: string // 종료일
+  displayText: string // 표시용 텍스트
+}
+
+export function CostCenterSelector({
+  selectedCode,
+  onCodeSelect,
+  disabled,
+  placeholder = "코스트센터를 선택하세요",
+  className
+}: CostCenterSelectorProps) {
+  const [open, setOpen] = useState(false)
+  const [codes, setCodes] = useState<CostCenter[]>([])
+  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 [isPending, startTransition] = useTransition()
+
+  // 날짜 포맷 함수 (YYYYMMDD -> YYYY-MM-DD)
+  const formatDate = (dateStr: string) => {
+    if (!dateStr || dateStr.length !== 8) return dateStr
+    return `${dateStr.substring(0, 4)}-${dateStr.substring(4, 6)}-${dateStr.substring(6, 8)}`
+  }
+
+  // Cost Center 선택 핸들러
+  const handleCodeSelect = useCallback(async (code: CostCenter) => {
+    onCodeSelect(code)
+    setOpen(false)
+  }, [onCodeSelect])
+
+  // 테이블 컬럼 정의
+  const columns: ColumnDef<CostCenter>[] = useMemo(() => [
+    {
+      accessorKey: 'KOSTL',
+      header: '코스트센터',
+      cell: ({ row }) => (
+        <div className="font-mono text-sm">{row.getValue('KOSTL')}</div>
+      ),
+    },
+    {
+      accessorKey: 'KTEXT',
+      header: '단축명',
+      cell: ({ row }) => (
+        <div>{row.getValue('KTEXT')}</div>
+      ),
+    },
+    {
+      accessorKey: 'LTEXT',
+      header: '설명',
+      cell: ({ row }) => (
+        <div>{row.getValue('LTEXT')}</div>
+      ),
+    },
+    {
+      accessorKey: 'DATAB',
+      header: '시작일',
+      cell: ({ row }) => (
+        <div className="text-sm">{formatDate(row.getValue('DATAB'))}</div>
+      ),
+    },
+    {
+      accessorKey: 'DATBI',
+      header: '종료일',
+      cell: ({ row }) => (
+        <div className="text-sm">{formatDate(row.getValue('DATBI'))}</div>
+      ),
+    },
+    {
+      id: 'actions',
+      header: '선택',
+      cell: ({ row }) => (
+        <Button
+          variant="ghost"
+          size="sm"
+          onClick={(e) => {
+            e.stopPropagation()
+            handleCodeSelect(row.original)
+          }}
+        >
+          <Check className="h-4 w-4" />
+        </Button>
+      ),
+    },
+  ], [handleCodeSelect])
+
+  // Cost Center 테이블 설정
+  const table = useReactTable({
+    data: codes,
+    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,
+    },
+  })
+
+  // 서버에서 Cost Center 전체 목록 로드 (한 번만)
+  const loadCodes = useCallback(async () => {
+    startTransition(async () => {
+      try {
+        const result = await getCostCenters()
+
+        if (result.success) {
+          setCodes(result.data)
+
+          // 폴백 데이터를 사용하는 경우 알림
+          if (result.isUsingFallback) {
+            toast.info('Oracle 연결 실패', {
+              description: '테스트 데이터를 사용합니다.',
+              duration: 4000,
+            })
+          }
+        } else {
+          toast.error(result.error || '코스트센터를 불러오는데 실패했습니다.')
+          setCodes([])
+        }
+      } catch (error) {
+        console.error('코스트센터 목록 로드 실패:', error)
+        toast.error('코스트센터를 불러오는 중 오류가 발생했습니다.')
+        setCodes([])
+      }
+    })
+  }, [])
+
+  // 다이얼로그 열기 핸들러
+  const handleDialogOpenChange = useCallback((newOpen: boolean) => {
+    setOpen(newOpen)
+    if (newOpen && codes.length === 0) {
+      loadCodes()
+    }
+  }, [loadCodes, codes.length])
+
+  // 검색어 변경 핸들러 (클라이언트 사이드 필터링)
+  const handleSearchChange = useCallback((value: string) => {
+    setGlobalFilter(value)
+  }, [])
+
+  return (
+    <Dialog open={open} onOpenChange={handleDialogOpenChange}>
+      <DialogTrigger asChild>
+        <Button
+          variant="outline"
+          disabled={disabled}
+          className={`w-full justify-start ${className || ''}`}
+        >
+          {selectedCode ? (
+            <div className="flex items-center gap-2 w-full">
+              <span className="font-mono text-sm">[{selectedCode.KOSTL}]</span>
+              <span className="truncate flex-1 text-left">{selectedCode.KTEXT}</span>
+            </div>
+          ) : (
+            <span className="text-muted-foreground">{placeholder}</span>
+          )}
+        </Button>
+      </DialogTrigger>
+      <DialogContent className="max-w-5xl max-h-[80vh]">
+        <DialogHeader>
+          <DialogTitle>코스트센터 선택</DialogTitle>
+          <div className="text-sm text-muted-foreground">
+            코스트센터 조회
+          </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) => handleSearchChange(e.target.value)}
+              className="flex-1"
+            />
+          </div>
+
+          {isPending ? (
+            <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={() => handleCodeSelect(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>
+  )
+}
+
-- 
cgit v1.2.3

