From 1e6d30c9f649dcaa0c1d24561af35d7a77fd51b2 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 18 Nov 2025 10:31:47 +0000 Subject: (최겸) 구매 법무검토 조회 dialog 개발 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/legal/sslvw-pur-inq-req-dialog.tsx | 333 +++++++++++++++++++++ lib/basic-contract/sslvw-service.ts | 82 +++++ ...basic-contract-detail-table-toolbar-actions.tsx | 135 +-------- 3 files changed, 420 insertions(+), 130 deletions(-) create mode 100644 components/common/legal/sslvw-pur-inq-req-dialog.tsx create mode 100644 lib/basic-contract/sslvw-service.ts 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([]) + const [error, setError] = React.useState(null) + const [rowSelection, setRowSelection] = React.useState>({}) + + 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[]>(() => { + if (data.length === 0) return [] + + const dataKeys = Object.keys(data[0]) + + return [ + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="모든 행 선택" + /> + ), + cell: ({ row }) => ( + 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 ( + + + + + + + 법무검토 요청 데이터 + + 법무검토 요청 데이터를 조회합니다. + {data.length > 0 && ` (${data.length}건, ${selectedRows.length}개 선택됨)`} + + + +
+ {isLoading ? ( +
+ + 데이터 로딩 중... +
+ ) : error ? ( +
+ 오류: {error} +
+ ) : data.length === 0 ? ( +
+ 데이터가 없습니다. +
+ ) : ( + <> + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + 데이터가 없습니다. + + + )} + +
+
+ + {/* 페이지네이션 컨트롤 */} +
+
+ {table.getFilteredSelectedRowModel().rows.length}개 행 선택됨 +
+
+
+

페이지당 행 수

+ +
+
+ {table.getState().pagination.pageIndex + 1} /{" "} + {table.getPageCount()} +
+
+ + + + +
+
+
+ + )} +
+ + + + {/* */} + + +
+
+ ) +} 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> + + 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 @@ -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([]) @@ -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 - {/* 법무검토 요청 버튼 */} - + {/* 법무검토 버튼 (SSLVW 데이터 조회) */} + {/* 최종승인 버튼 */}