From 8a0096dff6f16015ee12c8b25a6b8471733b6529 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Fri, 17 Oct 2025 07:16:43 +0000
Subject: (김준회) 조달담당자 선택기 구현 및 users 테이블에 조달담당자 컬럼
 추가
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../procurement-manager-single-selector.tsx        | 374 +++++++++++++++++++++
 1 file changed, 374 insertions(+)
 create mode 100644 components/common/selectors/procurement-manager/procurement-manager-single-selector.tsx

(limited to 'components/common/selectors/procurement-manager/procurement-manager-single-selector.tsx')

diff --git a/components/common/selectors/procurement-manager/procurement-manager-single-selector.tsx b/components/common/selectors/procurement-manager/procurement-manager-single-selector.tsx
new file mode 100644
index 00000000..4667385a
--- /dev/null
+++ b/components/common/selectors/procurement-manager/procurement-manager-single-selector.tsx
@@ -0,0 +1,374 @@
+'use client'
+
+/**
+ * 조달담당자 단일 선택 다이얼로그
+ * 
+ * @description
+ * - 조달담당자를 하나만 선택할 수 있는 다이얼로그
+ * - 트리거 버튼과 다이얼로그가 분리된 구조
+ * - 외부에서 open 상태를 제어 가능
+ * - 선택 시 사용자 정보도 함께 반환
+ */
+
+import { useState, useCallback, useMemo, useTransition, useEffect } from 'react'
+import { Button } from '@/components/ui/button'
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
+import { Input } from '@/components/ui/input'
+import { Search, Check, X } 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 { 
+  getProcurementManagers, 
+  addUsersToManagersAsync,
+  ProcurementManager, 
+  ProcurementManagerWithUser
+} from './procurement-manager-service'
+import { toast } from 'sonner'
+
+export interface ProcurementManagerSingleSelectorProps {
+  open: boolean
+  onOpenChange: (open: boolean) => void
+  selectedManager?: ProcurementManagerWithUser
+  onManagerSelect: (manager: ProcurementManagerWithUser) => void
+  onConfirm?: (manager: ProcurementManagerWithUser | undefined) => void
+  onCancel?: () => void
+  title?: string
+  description?: string
+  showConfirmButtons?: boolean
+}
+
+export function ProcurementManagerSingleSelector({ 
+  open,
+  onOpenChange,
+  selectedManager, 
+  onManagerSelect, 
+  onConfirm,
+  onCancel,
+  title = "조달담당자 선택",
+  description = "조달담당자를 선택하세요",
+  showConfirmButtons = false
+}: ProcurementManagerSingleSelectorProps) {
+  const [managers, setManagers] = useState<ProcurementManager[]>([])
+  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()
+  const [tempSelectedManager, setTempSelectedManager] = useState<ProcurementManagerWithUser | undefined>(selectedManager)
+
+  // 조달담당자 선택 핸들러
+  const handleManagerSelect = useCallback(async (manager: ProcurementManager) => {
+    try {
+      // 사용자 정보 추가
+      const [managerWithUser] = await addUsersToManagersAsync(manager)
+      if (showConfirmButtons) {
+        setTempSelectedManager(managerWithUser)
+      } else {
+        onManagerSelect(managerWithUser)
+        onOpenChange(false)
+      }
+    } catch (error) {
+      console.error('조달담당자 선택 오류:', error)
+      toast.error('조달담당자 선택 중 오류가 발생했습니다.')
+    }
+  }, [onManagerSelect, onOpenChange, showConfirmButtons])
+
+  // 확인 버튼 핸들러
+  const handleConfirm = useCallback(() => {
+    if (tempSelectedManager) {
+      onManagerSelect(tempSelectedManager)
+    }
+    onConfirm?.(tempSelectedManager)
+    onOpenChange(false)
+  }, [tempSelectedManager, onManagerSelect, onConfirm, onOpenChange])
+
+  // 취소 버튼 핸들러
+  const handleCancel = useCallback(() => {
+    setTempSelectedManager(selectedManager)
+    onCancel?.()
+    onOpenChange(false)
+  }, [selectedManager, onCancel, onOpenChange])
+
+  // 테이블 컬럼 정의
+  const columns: ColumnDef<ProcurementManager>[] = useMemo(() => [
+    {
+      accessorKey: 'PROCUREMENT_MANAGER_CODE',
+      header: '조달담당자코드',
+      cell: ({ row }) => (
+        <div className="font-mono text-sm">{row.getValue('PROCUREMENT_MANAGER_CODE')}</div>
+      ),
+    },
+    {
+      accessorKey: 'DISPLAY_NAME',
+      header: '담당자명',
+      cell: ({ row }) => (
+        <div>{row.getValue('DISPLAY_NAME')}</div>
+      ),
+    },
+    {
+      accessorKey: 'DEPARTMENT_NAME',
+      header: '부서명',
+      cell: ({ row }) => (
+        <div className="text-sm text-muted-foreground">{row.getValue('DEPARTMENT_NAME')}</div>
+      ),
+    },
+    {
+      id: 'actions',
+      header: '선택',
+      cell: ({ row }) => {
+        const isSelected = showConfirmButtons 
+          ? tempSelectedManager?.PROCUREMENT_MANAGER_CODE === row.original.PROCUREMENT_MANAGER_CODE
+          : selectedManager?.PROCUREMENT_MANAGER_CODE === row.original.PROCUREMENT_MANAGER_CODE
+        
+        return (
+          <Button
+            variant={isSelected ? "default" : "ghost"}
+            size="sm"
+            onClick={(e) => {
+              e.stopPropagation()
+              handleManagerSelect(row.original)
+            }}
+          >
+            <Check className="h-4 w-4" />
+          </Button>
+        )
+      },
+    },
+  ], [handleManagerSelect, selectedManager, tempSelectedManager, showConfirmButtons])
+
+  // 조달담당자 테이블 설정
+  const table = useReactTable({
+    data: managers,
+    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 loadManagers = useCallback(async () => {
+    startTransition(async () => {
+      try {
+        const result = await getProcurementManagers()
+        
+        if (result.success) {
+          setManagers(result.data)
+          
+          // 폴백 데이터를 사용하는 경우 알림
+          if (result.isUsingFallback) {
+            toast.info('Oracle 연결 실패', {
+              description: '테스트 데이터를 사용합니다.',
+              duration: 4000,
+            })
+          }
+        } else {
+          toast.error(result.error || '조달담당자를 불러오는데 실패했습니다.')
+          setManagers([])
+        }
+      } catch (error) {
+        console.error('조달담당자 목록 로드 실패:', error)
+        toast.error('조달담당자를 불러오는 중 오류가 발생했습니다.')
+        setManagers([])
+      }
+    })
+  }, [])
+
+  // 다이얼로그 열릴 때 코드 로드 (open prop 변화 감지)
+  useEffect(() => {
+    if (open) {
+      setTempSelectedManager(selectedManager)
+      if (managers.length === 0) {
+        console.log('🚀 [ProcurementManagerSingleSelector] 다이얼로그 열림 - loadManagers 호출')
+        loadManagers()
+      } else {
+        console.log('📦 [ProcurementManagerSingleSelector] 다이얼로그 열림 - 기존 데이터 사용 (' + managers.length + '건)')
+      }
+    }
+  }, [open, selectedManager, loadManagers, managers.length])
+
+  // 검색어 변경 핸들러 (클라이언트 사이드 필터링)
+  const handleSearchChange = useCallback((value: string) => {
+    setGlobalFilter(value)
+  }, [])
+
+  const currentSelectedManager = showConfirmButtons ? tempSelectedManager : selectedManager
+
+  return (
+    <Dialog open={open} onOpenChange={onOpenChange}>
+      <DialogContent className="max-w-5xl max-h-[80vh]">
+        <DialogHeader>
+          <DialogTitle>{title}</DialogTitle>
+          <div className="text-sm text-muted-foreground">
+            {description}
+          </div>
+        </DialogHeader>
+        
+        <div className="space-y-4">
+          {/* 현재 선택된 조달담당자 표시 */}
+          {currentSelectedManager && (
+            <div className="p-3 bg-muted rounded-md">
+              <div className="text-sm font-medium">선택된 조달담당자:</div>
+              <div className="flex items-center gap-2 mt-1">
+                <span className="font-mono text-sm">[{currentSelectedManager.PROCUREMENT_MANAGER_CODE}]</span>
+                <span>{currentSelectedManager.DISPLAY_NAME}</span>
+                {currentSelectedManager.DEPARTMENT_NAME && (
+                  <span className="text-muted-foreground">({currentSelectedManager.DEPARTMENT_NAME})</span>
+                )}
+                {currentSelectedManager.user && (
+                  <span className="text-muted-foreground text-xs">- {currentSelectedManager.user.name}</span>
+                )}
+              </div>
+            </div>
+          )}
+
+          <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) => {
+                      const isRowSelected = currentSelectedManager?.PROCUREMENT_MANAGER_CODE === row.original.PROCUREMENT_MANAGER_CODE
+                      return (
+                        <TableRow
+                          key={row.id}
+                          data-state={isRowSelected && "selected"}
+                          className={`cursor-pointer hover:bg-muted/50 ${
+                            isRowSelected ? 'bg-muted' : ''
+                          }`}
+                          onClick={() => handleManagerSelect(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>
+
+        {showConfirmButtons && (
+          <DialogFooter>
+            <Button variant="outline" onClick={handleCancel}>
+              <X className="h-4 w-4 mr-2" />
+              취소
+            </Button>
+            <Button onClick={handleConfirm} disabled={!tempSelectedManager}>
+              <Check className="h-4 w-4 mr-2" />
+              확인
+            </Button>
+          </DialogFooter>
+        )}
+      </DialogContent>
+    </Dialog>
+  )
+}
+
+
-- 
cgit v1.2.3

