summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--components/common/legal/sslvw-pur-inq-req-dialog.tsx333
-rw-r--r--lib/basic-contract/sslvw-service.ts82
-rw-r--r--lib/basic-contract/status-detail/basic-contract-detail-table-toolbar-actions.tsx135
3 files changed, 420 insertions, 130 deletions
diff --git a/components/common/legal/sslvw-pur-inq-req-dialog.tsx b/components/common/legal/sslvw-pur-inq-req-dialog.tsx
new file mode 100644
index 00000000..438b6582
--- /dev/null
+++ b/components/common/legal/sslvw-pur-inq-req-dialog.tsx
@@ -0,0 +1,333 @@
+"use client"
+
+import * as React from "react"
+import { Loader, Database, Check } from "lucide-react"
+import { toast } from "sonner"
+import {
+ useReactTable,
+ getCoreRowModel,
+ getPaginationRowModel,
+ getFilteredRowModel,
+ ColumnDef,
+ flexRender,
+} from "@tanstack/react-table"
+
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog"
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table"
+import { Checkbox } from "@/components/ui/checkbox"
+import { ScrollArea } from "@/components/ui/scroll-area"
+
+import { getSSLVWPurInqReqData } from "@/lib/basic-contract/sslvw-service"
+import { SSLVWPurInqReq } from "@/lib/basic-contract/sslvw-service"
+
+interface SSLVWPurInqReqDialogProps {
+ onConfirm?: (selectedRows: SSLVWPurInqReq[]) => void
+}
+
+export function SSLVWPurInqReqDialog({ onConfirm }: SSLVWPurInqReqDialogProps) {
+ const [open, setOpen] = React.useState(false)
+ const [isLoading, setIsLoading] = React.useState(false)
+ const [data, setData] = React.useState<SSLVWPurInqReq[]>([])
+ const [error, setError] = React.useState<string | null>(null)
+ const [rowSelection, setRowSelection] = React.useState<Record<string, boolean>>({})
+
+ const loadData = async () => {
+ setIsLoading(true)
+ setError(null)
+ try {
+ const result = await getSSLVWPurInqReqData()
+ if (result.success) {
+ setData(result.data)
+ if (result.isUsingFallback) {
+ toast.info("테스트 데이터를 표시합니다.")
+ }
+ } else {
+ setError(result.error || "데이터 로딩 실패")
+ toast.error(result.error || "데이터 로딩 실패")
+ }
+ } catch (err) {
+ const errorMessage = err instanceof Error ? err.message : "알 수 없는 오류"
+ setError(errorMessage)
+ toast.error(errorMessage)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ React.useEffect(() => {
+ if (open) {
+ loadData()
+ } else {
+ // 다이얼로그 닫힐 때 데이터 초기화
+ setData([])
+ setError(null)
+ setRowSelection({})
+ }
+ }, [open])
+
+ // 테이블 컬럼 정의 (동적 생성)
+ const columns = React.useMemo<ColumnDef<SSLVWPurInqReq>[]>(() => {
+ if (data.length === 0) return []
+
+ const dataKeys = Object.keys(data[0])
+
+ return [
+ {
+ id: "select",
+ header: ({ table }) => (
+ <Checkbox
+ checked={
+ table.getIsAllPageRowsSelected() ||
+ (table.getIsSomePageRowsSelected() && "indeterminate")
+ }
+ onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
+ aria-label="모든 행 선택"
+ />
+ ),
+ cell: ({ row }) => (
+ <Checkbox
+ checked={row.getIsSelected()}
+ onCheckedChange={(value) => row.toggleSelected(!!value)}
+ aria-label="행 선택"
+ />
+ ),
+ enableSorting: false,
+ enableHiding: false,
+ },
+ ...dataKeys.map((key) => ({
+ accessorKey: key,
+ header: key,
+ cell: ({ getValue }: any) => {
+ const value = getValue()
+ return value !== null && value !== undefined ? String(value) : ""
+ },
+ })),
+ ]
+ }, [data])
+
+ // 테이블 인스턴스 생성
+ const table = useReactTable({
+ data,
+ columns,
+ getCoreRowModel: getCoreRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ getFilteredRowModel: getFilteredRowModel(),
+ onRowSelectionChange: setRowSelection,
+ state: {
+ rowSelection,
+ },
+ })
+
+ // 선택된 행들 가져오기
+ const selectedRows = table.getFilteredSelectedRowModel().rows.map(row => row.original)
+
+ // 확인 버튼 핸들러
+ const handleConfirm = () => {
+ if (selectedRows.length === 0) {
+ toast.error("행을 선택해주세요.")
+ return
+ }
+
+ if (onConfirm) {
+ onConfirm(selectedRows)
+ toast.success(`${selectedRows.length}개의 행을 선택했습니다.`)
+ } else {
+ // 임시로 선택된 데이터 콘솔 출력
+ console.log("선택된 행들:", selectedRows)
+ toast.success(`${selectedRows.length}개의 행이 선택되었습니다. (콘솔 확인)`)
+ }
+
+ setOpen(false)
+ }
+
+ return (
+ <Dialog open={open} onOpenChange={setOpen}>
+ <DialogTrigger asChild>
+ <Button variant="outline" size="sm">
+ <Database className="mr-2 size-4" aria-hidden="true" />
+ 법무검토 요청 데이터 조회
+ </Button>
+ </DialogTrigger>
+ <DialogContent className="max-w-7xl max-h-[90vh] flex flex-col">
+ <DialogHeader>
+ <DialogTitle>법무검토 요청 데이터</DialogTitle>
+ <DialogDescription>
+ 법무검토 요청 데이터를 조회합니다.
+ {data.length > 0 && ` (${data.length}건, ${selectedRows.length}개 선택됨)`}
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="flex-1 overflow-hidden flex flex-col">
+ {isLoading ? (
+ <div className="flex items-center justify-center flex-1">
+ <Loader className="mr-2 size-6 animate-spin" />
+ <span>데이터 로딩 중...</span>
+ </div>
+ ) : error ? (
+ <div className="flex items-center justify-center flex-1 text-red-500">
+ <span>오류: {error}</span>
+ </div>
+ ) : data.length === 0 ? (
+ <div className="flex items-center justify-center flex-1 text-muted-foreground">
+ <span>데이터가 없습니다.</span>
+ </div>
+ ) : (
+ <>
+ <ScrollArea className="flex-1">
+ <Table>
+ <TableHeader>
+ {table.getHeaderGroups().map((headerGroup) => (
+ <TableRow key={headerGroup.id}>
+ {headerGroup.headers.map((header) => (
+ <TableHead key={header.id} className="font-medium">
+ {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"}
+ >
+ {row.getVisibleCells().map((cell) => (
+ <TableCell key={cell.id} className="text-sm">
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext()
+ )}
+ </TableCell>
+ ))}
+ </TableRow>
+ ))
+ ) : (
+ <TableRow>
+ <TableCell
+ colSpan={columns.length}
+ className="h-24 text-center"
+ >
+ 데이터가 없습니다.
+ </TableCell>
+ </TableRow>
+ )}
+ </TableBody>
+ </Table>
+ </ScrollArea>
+
+ {/* 페이지네이션 컨트롤 */}
+ <div className="flex items-center justify-between px-2 py-4 border-t">
+ <div className="flex-1 text-sm text-muted-foreground">
+ {table.getFilteredSelectedRowModel().rows.length}개 행 선택됨
+ </div>
+ <div className="flex items-center space-x-6 lg:space-x-8">
+ <div className="flex items-center space-x-2">
+ <p className="text-sm font-medium">페이지당 행 수</p>
+ <select
+ value={table.getState().pagination.pageSize}
+ onChange={(e) => {
+ table.setPageSize(Number(e.target.value))
+ }}
+ className="h-8 w-[70px] rounded border border-input bg-transparent px-3 py-1 text-sm ring-offset-background focus:ring-2 focus:ring-ring focus:ring-offset-2"
+ >
+ {[10, 20, 30, 40, 50].map((pageSize) => (
+ <option key={pageSize} value={pageSize}>
+ {pageSize}
+ </option>
+ ))}
+ </select>
+ </div>
+ <div className="flex w-[100px] items-center justify-center text-sm font-medium">
+ {table.getState().pagination.pageIndex + 1} /{" "}
+ {table.getPageCount()}
+ </div>
+ <div className="flex items-center space-x-2">
+ <Button
+ variant="outline"
+ className="h-8 w-8 p-0"
+ onClick={() => table.setPageIndex(0)}
+ disabled={!table.getCanPreviousPage()}
+ >
+ <span className="sr-only">첫 페이지로</span>
+ {"<<"}
+ </Button>
+ <Button
+ variant="outline"
+ className="h-8 w-8 p-0"
+ onClick={() => table.previousPage()}
+ disabled={!table.getCanPreviousPage()}
+ >
+ <span className="sr-only">이전 페이지</span>
+ {"<"}
+ </Button>
+ <Button
+ variant="outline"
+ className="h-8 w-8 p-0"
+ onClick={() => table.nextPage()}
+ disabled={!table.getCanNextPage()}
+ >
+ <span className="sr-only">다음 페이지</span>
+ {">"}
+ </Button>
+ <Button
+ variant="outline"
+ className="h-8 w-8 p-0"
+ onClick={() => table.setPageIndex(table.getPageCount() - 1)}
+ disabled={!table.getCanNextPage()}
+ >
+ <span className="sr-only">마지막 페이지로</span>
+ {">>"}
+ </Button>
+ </div>
+ </div>
+ </div>
+ </>
+ )}
+ </div>
+
+ <DialogFooter className="gap-2">
+ <Button variant="outline" onClick={() => setOpen(false)}>
+ 닫기
+ </Button>
+ {/* <Button onClick={loadData} disabled={isLoading} variant="outline">
+ {isLoading ? (
+ <>
+ <Loader className="mr-2 size-4 animate-spin" />
+ 로딩 중...
+ </>
+ ) : (
+ "새로고침"
+ )}
+ </Button> */}
+ <Button onClick={handleConfirm} disabled={selectedRows.length === 0}>
+ <Check className="mr-2 size-4" />
+ 확인 ({selectedRows.length})
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )
+}
diff --git a/lib/basic-contract/sslvw-service.ts b/lib/basic-contract/sslvw-service.ts
new file mode 100644
index 00000000..9650d43a
--- /dev/null
+++ b/lib/basic-contract/sslvw-service.ts
@@ -0,0 +1,82 @@
+"use server"
+
+import { oracleKnex } from '@/lib/oracle-db/db'
+
+// SSLVW_PUR_INQ_REQ 테이블 데이터 타입 (실제 테이블 구조에 맞게 조정 필요)
+export interface SSLVWPurInqReq {
+ [key: string]: string | number | Date | null | undefined
+}
+
+// 테스트 환경용 폴백 데이터
+const FALLBACK_TEST_DATA: SSLVWPurInqReq[] = [
+ {
+ id: 1,
+ request_number: 'REQ001',
+ status: 'PENDING',
+ created_date: new Date('2025-01-01'),
+ description: '테스트 요청 1'
+ },
+ {
+ id: 2,
+ request_number: 'REQ002',
+ status: 'APPROVED',
+ created_date: new Date('2025-01-02'),
+ description: '테스트 요청 2'
+ }
+]
+
+/**
+ * SSLVW_PUR_INQ_REQ 테이블 전체 조회
+ * @returns 테이블 데이터 배열
+ */
+export async function getSSLVWPurInqReqData(): Promise<{
+ success: boolean
+ data: SSLVWPurInqReq[]
+ error?: string
+ isUsingFallback?: boolean
+}> {
+ try {
+ console.log('📋 [getSSLVWPurInqReqData] SSLVW_PUR_INQ_REQ 테이블 조회 시작...')
+
+ const result = await oracleKnex.raw(`
+ SELECT *
+ FROM SSLVW_PUR_INQ_REQ
+ WHERE ROWNUM < 100
+ ORDER BY 1
+ `)
+
+ // Oracle raw query의 결과는 rows 배열에 들어있음
+ const rows = (result.rows || result) as Array<Record<string, unknown>>
+
+ console.log(`✅ [getSSLVWPurInqReqData] 조회 성공 - ${rows.length}건`)
+
+ // 데이터 타입 변환 (필요에 따라 조정)
+ const cleanedResult = rows.map((item) => {
+ const convertedItem: SSLVWPurInqReq = {}
+ for (const [key, value] of Object.entries(item)) {
+ if (value instanceof Date) {
+ convertedItem[key] = value
+ } else if (value === null) {
+ convertedItem[key] = null
+ } else {
+ convertedItem[key] = String(value)
+ }
+ }
+ return convertedItem
+ })
+
+ return {
+ success: true,
+ data: cleanedResult,
+ isUsingFallback: false
+ }
+ } catch (error) {
+ console.error('❌ [getSSLVWPurInqReqData] 오류:', error)
+ console.log('🔄 [getSSLVWPurInqReqData] 폴백 테스트 데이터 사용')
+ return {
+ success: true,
+ data: FALLBACK_TEST_DATA,
+ isUsingFallback: true
+ }
+ }
+}
diff --git a/lib/basic-contract/status-detail/basic-contract-detail-table-toolbar-actions.tsx b/lib/basic-contract/status-detail/basic-contract-detail-table-toolbar-actions.tsx
index 37ae135c..c71be9d1 100644
--- a/lib/basic-contract/status-detail/basic-contract-detail-table-toolbar-actions.tsx
+++ b/lib/basic-contract/status-detail/basic-contract-detail-table-toolbar-actions.tsx
@@ -2,7 +2,7 @@
import * as React from "react"
import { type Table } from "@tanstack/react-table"
-import { Download, FileDown, Mail, Scale, CheckCircle, AlertTriangle, Send, Gavel, Check, FileSignature } from "lucide-react"
+import { Download, FileDown, Mail, CheckCircle, AlertTriangle, Send, Check, FileSignature } from "lucide-react"
import { exportTableToExcel } from "@/lib/export"
import { downloadFile } from "@/lib/file-download"
@@ -18,11 +18,9 @@ import {
DialogTitle,
} from "@/components/ui/dialog"
import { Badge } from "@/components/ui/badge"
-import { Textarea } from "@/components/ui/textarea"
-import { Label } from "@/components/ui/label"
-import { Separator } from "@/components/ui/separator"
-import { prepareFinalApprovalAction, quickFinalApprovalAction, requestLegalReviewAction, resendContractsAction } from "../service"
+import { prepareFinalApprovalAction, quickFinalApprovalAction, resendContractsAction } from "../service"
import { BasicContractSignDialog } from "../vendor-table/basic-contract-sign-dialog"
+import { SSLVWPurInqReqDialog } from "@/components/common/legal/sslvw-pur-inq-req-dialog"
interface BasicContractDetailTableToolbarActionsProps {
table: Table<BasicContractView>
@@ -35,10 +33,8 @@ export function BasicContractDetailTableToolbarActions({ table }: BasicContractD
// 다이얼로그 상태
const [resendDialog, setResendDialog] = React.useState(false)
- const [legalReviewDialog, setLegalReviewDialog] = React.useState(false)
const [finalApproveDialog, setFinalApproveDialog] = React.useState(false)
const [loading, setLoading] = React.useState(false)
- const [reviewNote, setReviewNote] = React.useState("")
const [buyerSignDialog, setBuyerSignDialog] = React.useState(false)
const [contractsToSign, setContractsToSign] = React.useState<any[]>([])
@@ -49,10 +45,6 @@ export function BasicContractDetailTableToolbarActions({ table }: BasicContractD
const canBulkResend = hasSelectedRows
- const canRequestLegalReview = hasSelectedRows && selectedRows.some(row =>
- row.original.legalReviewRequired && !row.original.legalReviewRequestedAt
- )
-
const canFinalApprove = hasSelectedRows && selectedRows.some(row => {
const contract = row.original;
if (contract.completedAt !== null || !contract.signedFilePath) {
@@ -67,10 +59,6 @@ export function BasicContractDetailTableToolbarActions({ table }: BasicContractD
// 필터링된 계약서들 계산
const resendContracts = selectedRows.map(row => row.original)
- const legalReviewContracts = selectedRows
- .map(row => row.original)
- .filter(contract => contract.legalReviewRequired && !contract.legalReviewRequestedAt)
-
const finalApproveContracts = selectedRows
.map(row => row.original)
.filter(contract => {
@@ -204,15 +192,6 @@ export function BasicContractDetailTableToolbarActions({ table }: BasicContractD
})
}
- // 법무검토 요청
- const handleLegalReviewRequest = async () => {
- if (!canRequestLegalReview) {
- toast.error("법무검토 요청 가능한 계약서를 선택해주세요")
- return
- }
- setLegalReviewDialog(true)
- }
-
// 최종승인
const handleFinalApprove = async () => {
if (!canFinalApprove) {
@@ -241,26 +220,6 @@ export function BasicContractDetailTableToolbarActions({ table }: BasicContractD
}
}
- // 법무검토 요청 확인
- const confirmLegalReview = async () => {
- setLoading(true)
- try {
- // TODO: 서버액션 호출
- await requestLegalReviewAction(legalReviewContracts.map(c => c.id), reviewNote)
-
- console.log("법무검토 요청:", legalReviewContracts, "메모:", reviewNote)
- toast.success(`${legalReviewContracts.length}건의 법무검토 요청을 완료했습니다`)
- setLegalReviewDialog(false)
- setReviewNote("")
- table.toggleAllPageRowsSelected(false) // 선택 해제
- } catch (error) {
- toast.error("법무검토 요청 중 오류가 발생했습니다")
- console.error(error)
- } finally {
- setLoading(false)
- }
- }
-
// 최종승인 확인 (수정됨)
const confirmFinalApprove = async () => {
setLoading(true)
@@ -354,25 +313,8 @@ export function BasicContractDetailTableToolbarActions({ table }: BasicContractD
</span>
</Button>
- {/* 법무검토 요청 버튼 */}
- <Button
- variant="outline"
- size="sm"
- onClick={handleLegalReviewRequest}
- disabled={!canRequestLegalReview}
- className="gap-2"
- title={!hasSelectedRows
- ? "계약서를 선택해주세요"
- : !canRequestLegalReview
- ? "법무검토 요청 가능한 계약서가 없습니다"
- : `${legalReviewContracts.length}건 법무검토 요청`
- }
- >
- <Scale className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">
- 법무검토 {hasSelectedRows ? `(${selectedRows.length})` : ''}
- </span>
- </Button>
+ {/* 법무검토 버튼 (SSLVW 데이터 조회) */}
+ <SSLVWPurInqReqDialog />
{/* 최종승인 버튼 */}
<Button
@@ -471,73 +413,6 @@ export function BasicContractDetailTableToolbarActions({ table }: BasicContractD
</DialogContent>
</Dialog>
- {/* 법무검토 요청 다이얼로그 */}
- <Dialog open={legalReviewDialog} onOpenChange={setLegalReviewDialog}>
- <DialogContent className="max-w-2xl">
- <DialogHeader>
- <DialogTitle className="flex items-center gap-2">
- <Gavel className="size-5" />
- 법무검토 요청
- </DialogTitle>
- <DialogDescription>
- 선택한 {legalReviewContracts.length}건의 계약서에 대한 법무검토를 요청합니다.
- </DialogDescription>
- </DialogHeader>
-
- <div className="space-y-4">
- <div className="max-h-48 overflow-y-auto">
- <div className="space-y-3">
- {legalReviewContracts.map((contract, index) => (
- <div key={contract.id} className="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
- <div className="flex-1">
- <div className="font-medium">{contract.vendorName || '업체명 없음'}</div>
- <div className="text-sm text-gray-500">
- {contract.vendorCode || '코드 없음'} | {contract.templateName || '템플릿명 없음'}
- </div>
- </div>
- <Badge variant="secondary">{contract.status}</Badge>
- </div>
- ))}
- </div>
- </div>
-
- <Separator />
-
- <div className="space-y-2">
- <Label htmlFor="review-note">검토 요청 메모 (선택사항)</Label>
- <Textarea
- id="review-note"
- placeholder="법무팀에게 전달할 특별한 요청사항이나 검토 포인트를 입력해주세요..."
- value={reviewNote}
- onChange={(e) => setReviewNote(e.target.value)}
- rows={3}
- />
- </div>
- </div>
-
- <DialogFooter>
- <Button
- variant="outline"
- onClick={() => {
- setLegalReviewDialog(false)
- setReviewNote("")
- }}
- disabled={loading}
- >
- 취소
- </Button>
- <Button
- onClick={confirmLegalReview}
- disabled={loading}
- className="gap-2"
- >
- <Gavel className="size-4" />
- {loading ? "요청 중..." : `${legalReviewContracts.length}건 검토요청`}
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
-
{/* 최종승인 다이얼로그 */}
<Dialog open={finalApproveDialog} onOpenChange={setFinalApproveDialog}>
<DialogContent className="max-w-2xl">