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

---
 .../gl-account/gl-account-single-selector.tsx      | 358 +++++++++++++++++++++
 1 file changed, 358 insertions(+)
 create mode 100644 components/common/selectors/gl-account/gl-account-single-selector.tsx

(limited to 'components/common/selectors/gl-account/gl-account-single-selector.tsx')

diff --git a/components/common/selectors/gl-account/gl-account-single-selector.tsx b/components/common/selectors/gl-account/gl-account-single-selector.tsx
new file mode 100644
index 00000000..2a6a7915
--- /dev/null
+++ b/components/common/selectors/gl-account/gl-account-single-selector.tsx
@@ -0,0 +1,358 @@
+'use client'
+
+/**
+ * GL 계정 단일 선택 다이얼로그
+ *
+ * @description
+ * - GL 계정을 하나만 선택할 수 있는 다이얼로그
+ * - 트리거 버튼과 다이얼로그가 분리된 구조
+ * - 외부에서 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 {
+  getGlAccounts,
+  GlAccount
+} from './gl-account-service'
+import { toast } from 'sonner'
+
+export interface GlAccountSingleSelectorProps {
+  open: boolean
+  onOpenChange: (open: boolean) => void
+  selectedCode?: GlAccount
+  onCodeSelect: (code: GlAccount) => void
+  onConfirm?: (code: GlAccount | undefined) => void
+  onCancel?: () => void
+  title?: string
+  description?: string
+  showConfirmButtons?: boolean
+}
+
+export function GlAccountSingleSelector({
+  open,
+  onOpenChange,
+  selectedCode,
+  onCodeSelect,
+  onConfirm,
+  onCancel,
+  title = "GL 계정 선택",
+  description = "GL 계정을 선택하세요",
+  showConfirmButtons = false
+}: GlAccountSingleSelectorProps) {
+  const [codes, setCodes] = useState<GlAccount[]>([])
+  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 [tempSelectedCode, setTempSelectedCode] = useState<GlAccount | undefined>(selectedCode)
+
+  // GL 계정 선택 핸들러
+  const handleCodeSelect = useCallback((code: GlAccount) => {
+    if (showConfirmButtons) {
+      setTempSelectedCode(code)
+    } else {
+      onCodeSelect(code)
+      onOpenChange(false)
+    }
+  }, [onCodeSelect, onOpenChange, showConfirmButtons])
+
+  // 확인 버튼 핸들러
+  const handleConfirm = useCallback(() => {
+    if (tempSelectedCode) {
+      onCodeSelect(tempSelectedCode)
+    }
+    onConfirm?.(tempSelectedCode)
+    onOpenChange(false)
+  }, [tempSelectedCode, onCodeSelect, onConfirm, onOpenChange])
+
+  // 취소 버튼 핸들러
+  const handleCancel = useCallback(() => {
+    setTempSelectedCode(selectedCode)
+    onCancel?.()
+    onOpenChange(false)
+  }, [selectedCode, onCancel, onOpenChange])
+
+  // 테이블 컬럼 정의
+  const columns: ColumnDef<GlAccount>[] = useMemo(() => [
+    {
+      accessorKey: 'SAKNR',
+      header: '계정(G/L)',
+      cell: ({ row }) => (
+        <div className="font-mono text-sm">{row.getValue('SAKNR')}</div>
+      ),
+    },
+    {
+      accessorKey: 'FIPEX',
+      header: '세부계정',
+      cell: ({ row }) => (
+        <div className="font-mono text-sm">{row.getValue('FIPEX')}</div>
+      ),
+    },
+    {
+      accessorKey: 'TEXT1',
+      header: '계정명',
+      cell: ({ row }) => (
+        <div>{row.getValue('TEXT1')}</div>
+      ),
+    },
+    {
+      id: 'actions',
+      header: '선택',
+      cell: ({ row }) => {
+        const isSelected = showConfirmButtons
+          ? tempSelectedCode?.SAKNR === row.original.SAKNR
+          : selectedCode?.SAKNR === row.original.SAKNR
+
+        return (
+          <Button
+            variant={isSelected ? "default" : "ghost"}
+            size="sm"
+            onClick={(e) => {
+              e.stopPropagation()
+              handleCodeSelect(row.original)
+            }}
+          >
+            <Check className="h-4 w-4" />
+          </Button>
+        )
+      },
+    },
+  ], [handleCodeSelect, selectedCode, tempSelectedCode, showConfirmButtons])
+
+  // GL 계정 테이블 설정
+  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,
+    },
+  })
+
+  // 서버에서 GL 계정 전체 목록 로드 (한 번만)
+  const loadCodes = useCallback(async () => {
+    startTransition(async () => {
+      try {
+        const result = await getGlAccounts()
+
+        if (result.success) {
+          setCodes(result.data)
+
+          // 폴백 데이터를 사용하는 경우 알림
+          if (result.isUsingFallback) {
+            toast.info('Oracle 연결 실패', {
+              description: '테스트 데이터를 사용합니다.',
+              duration: 4000,
+            })
+          }
+        } else {
+          toast.error(result.error || 'GL 계정을 불러오는데 실패했습니다.')
+          setCodes([])
+        }
+      } catch (error) {
+        console.error('GL 계정 목록 로드 실패:', error)
+        toast.error('GL 계정을 불러오는 중 오류가 발생했습니다.')
+        setCodes([])
+      }
+    })
+  }, [])
+
+  // 다이얼로그 열릴 때 코드 로드 (open prop 변화 감지)
+  useEffect(() => {
+    if (open) {
+      setTempSelectedCode(selectedCode)
+      if (codes.length === 0) {
+        console.log('🚀 [GlAccountSingleSelector] 다이얼로그 열림 - loadCodes 호출')
+        loadCodes()
+      } else {
+        console.log('📦 [GlAccountSingleSelector] 다이얼로그 열림 - 기존 데이터 사용 (' + codes.length + '건)')
+      }
+    }
+  }, [open, selectedCode, loadCodes, codes.length])
+
+  // 검색어 변경 핸들러 (클라이언트 사이드 필터링)
+  const handleSearchChange = useCallback((value: string) => {
+    setGlobalFilter(value)
+  }, [])
+
+  const currentSelectedCode = showConfirmButtons ? tempSelectedCode : selectedCode
+
+  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">
+          {/* 현재 선택된 GL 계정 표시 */}
+          {currentSelectedCode && (
+            <div className="p-3 bg-muted rounded-md">
+              <div className="text-sm font-medium">선택된 GL 계정:</div>
+              <div className="flex items-center gap-2 mt-1">
+                <span className="font-mono text-sm">[{currentSelectedCode.SAKNR}]</span>
+                <span className="font-mono text-sm">{currentSelectedCode.FIPEX}</span>
+                <span>- {currentSelectedCode.TEXT1}</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">GL 계정을 불러오는 중...</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 = currentSelectedCode?.SAKNR === row.original.SAKNR
+                      return (
+                        <TableRow
+                          key={row.id}
+                          data-state={isRowSelected && "selected"}
+                          className={`cursor-pointer hover:bg-muted/50 ${
+                            isRowSelected ? 'bg-muted' : ''
+                          }`}
+                          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}개 GL 계정
+            </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={!tempSelectedCode}>
+              <Check className="h-4 w-4 mr-2" />
+              확인
+            </Button>
+          </DialogFooter>
+        )}
+      </DialogContent>
+    </Dialog>
+  )
+}
+
-- 
cgit v1.2.3

