summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-12-04 09:08:44 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-12-04 09:08:44 +0000
commit0e1a15c1be7bd9620fc61767b63b5b6f87563b4f (patch)
tree86cf1a796f03f6846e34b07574a1ead35b086750
parent25749225689c3934bc10ad1e8285e13020b61282 (diff)
(임수민) 준법문의,법무검토 관련 요청사항 작업
-rw-r--r--components/common/legal/cpvw-wab-qust-list-view-dialog.tsx364
-rw-r--r--db/schema/basicContractDocumnet.ts15
-rw-r--r--lib/basic-contract/cpvw-service.ts236
-rw-r--r--lib/basic-contract/service.ts267
-rw-r--r--lib/basic-contract/sslvw-service.ts126
-rw-r--r--lib/basic-contract/status-detail/basic-contract-detail-table-toolbar-actions.tsx209
-rw-r--r--lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx39
-rw-r--r--types/table.d.ts2
8 files changed, 1184 insertions, 74 deletions
diff --git a/components/common/legal/cpvw-wab-qust-list-view-dialog.tsx b/components/common/legal/cpvw-wab-qust-list-view-dialog.tsx
new file mode 100644
index 00000000..aeefbb84
--- /dev/null
+++ b/components/common/legal/cpvw-wab-qust-list-view-dialog.tsx
@@ -0,0 +1,364 @@
+"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, ScrollBar } from "@/components/ui/scroll-area"
+
+import { getCPVWWabQustListViewData, CPVWWabQustListView } from "@/lib/basic-contract/cpvw-service"
+
+interface CPVWWabQustListViewDialogProps {
+ onConfirm?: (selectedRows: CPVWWabQustListView[]) => void
+ requireSingleSelection?: boolean
+ triggerDisabled?: boolean
+ triggerTitle?: string
+}
+
+export function CPVWWabQustListViewDialog({
+ onConfirm,
+ requireSingleSelection = false,
+ triggerDisabled = false,
+ triggerTitle,
+}: CPVWWabQustListViewDialogProps) {
+ const [open, setOpen] = React.useState(false)
+ const [isLoading, setIsLoading] = React.useState(false)
+ const [data, setData] = React.useState<CPVWWabQustListView[]>([])
+ 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 getCPVWWabQustListViewData()
+ 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<CPVWWabQustListView>[]>(() => {
+ 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 (requireSingleSelection && selectedRows.length !== 1) {
+ toast.error("하나의 행만 선택해주세요.")
+ return
+ }
+
+ if (onConfirm) {
+ onConfirm(selectedRows)
+ toast.success(
+ requireSingleSelection
+ ? "선택한 행으로 준법문의 상태를 동기화합니다."
+ : `${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"
+ disabled={triggerDisabled}
+ title={triggerTitle}
+ >
+ <Database className="mr-2 size-4" aria-hidden="true" />
+ 준법문의 요청 데이터 조회
+ </Button>
+ </DialogTrigger>
+ <DialogContent className="max-w-7xl h-[90vh] flex flex-col overflow-hidden">
+ <DialogHeader>
+ <DialogTitle>준법문의 요청 데이터</DialogTitle>
+ <DialogDescription>
+ 준법문의 요청 데이터를 조회합니다.
+ {data.length > 0 && ` (${data.length}건, ${selectedRows.length}개 선택됨)`}
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="flex flex-col flex-1 min-h-0">
+ {isLoading ? (
+ <div className="flex items-center justify-center flex-1 min-h-[200px]">
+ <Loader className="mr-2 size-6 animate-spin" />
+ <span>데이터 로딩 중...</span>
+ </div>
+ ) : error ? (
+ <div className="flex items-center justify-center flex-1 min-h-[200px] text-red-500">
+ <span>오류: {error}</span>
+ </div>
+ ) : data.length === 0 ? (
+ <div className="flex items-center justify-center flex-1 min-h-[200px] text-muted-foreground">
+ <span>데이터가 없습니다.</span>
+ </div>
+ ) : (
+ <div className="flex flex-col flex-1 min-h-0">
+ {/* 테이블 영역 - 스크롤 가능 */}
+ <ScrollArea className="flex-1 overflow-auto border rounded-md">
+ <Table className="min-w-full">
+ <TableHeader className="sticky top-0 bg-background z-10">
+ {table.getHeaderGroups().map((headerGroup) => (
+ <TableRow key={headerGroup.id}>
+ {headerGroup.headers.map((header) => (
+ <TableHead key={header.id} className="font-medium bg-background">
+ {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>
+ <ScrollBar orientation="horizontal" />
+ </ScrollArea>
+
+ {/* 페이지네이션 컨트롤 - 고정 영역 */}
+ <div className="flex items-center justify-between px-2 py-4 border-t bg-background flex-shrink-0">
+ <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>
+ )}
+ </div>
+
+ <DialogFooter className="gap-2 flex-shrink-0">
+ <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={
+ requireSingleSelection
+ ? selectedRows.length !== 1
+ : selectedRows.length === 0
+ }
+ >
+ <Check className="mr-2 size-4" />
+ 확인 ({selectedRows.length})
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )
+}
+
diff --git a/db/schema/basicContractDocumnet.ts b/db/schema/basicContractDocumnet.ts
index 944c4b2c..e571c7e0 100644
--- a/db/schema/basicContractDocumnet.ts
+++ b/db/schema/basicContractDocumnet.ts
@@ -67,6 +67,12 @@ export const basicContract = pgTable('basic_contract', {
legalReviewRegNo: varchar('legal_review_reg_no', { length: 100 }), // 법무 시스템 REG_NO
legalReviewProgressStatus: varchar('legal_review_progress_status', { length: 255 }), // PRGS_STAT_DSC 값
+ // 준법문의 관련 필드
+ complianceReviewRequestedAt: timestamp('compliance_review_requested_at'), // 준법문의 요청일
+ complianceReviewCompletedAt: timestamp('compliance_review_completed_at'), // 준법문의 완료일
+ complianceReviewRegNo: varchar('compliance_review_reg_no', { length: 100 }), // 준법문의 시스템 REG_NO
+ complianceReviewProgressStatus: varchar('compliance_review_progress_status', { length: 255 }), // 준법문의 PRGS_STAT_DSC 값
+
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
completedAt: timestamp('completed_at'), // 계약 체결 완료 날짜
@@ -99,6 +105,12 @@ export const basicContractView = pgView('basic_contract_view').as((qb) => {
legalReviewRegNo: sql<string | null>`${basicContract.legalReviewRegNo}`.as('legal_review_reg_no'),
legalReviewProgressStatus: sql<string | null>`${basicContract.legalReviewProgressStatus}`.as('legal_review_progress_status'),
+ // 준법문의 관련 필드
+ complianceReviewRequestedAt: sql<Date | null>`${basicContract.complianceReviewRequestedAt}`.as('compliance_review_requested_at'),
+ complianceReviewCompletedAt: sql<Date | null>`${basicContract.complianceReviewCompletedAt}`.as('compliance_review_completed_at'),
+ complianceReviewRegNo: sql<string | null>`${basicContract.complianceReviewRegNo}`.as('compliance_review_reg_no'),
+ complianceReviewProgressStatus: sql<string | null>`${basicContract.complianceReviewProgressStatus}`.as('compliance_review_progress_status'),
+
createdAt: sql<Date>`${basicContract.createdAt}`.as('created_at'),
updatedAt: sql<Date>`${basicContract.updatedAt}`.as('updated_at'),
completedAt: sql<Date | null>`${basicContract.completedAt}`.as('completed_at'),
@@ -121,6 +133,9 @@ export const basicContractView = pgView('basic_contract_view').as((qb) => {
// 법무검토 상태 (PRGS_STAT_DSC 동기화 값)
legalReviewStatus: sql<string | null>`${basicContract.legalReviewProgressStatus}`.as('legal_review_status'),
+
+ // 준법문의 상태 (PRGS_STAT_DSC 동기화 값)
+ complianceReviewStatus: sql<string | null>`${basicContract.complianceReviewProgressStatus}`.as('compliance_review_status'),
// 템플릿 파일 정보
templateFilePath: sql<string | null>`${basicContractTemplates.filePath}`.as('template_file_path'),
diff --git a/lib/basic-contract/cpvw-service.ts b/lib/basic-contract/cpvw-service.ts
new file mode 100644
index 00000000..6d249002
--- /dev/null
+++ b/lib/basic-contract/cpvw-service.ts
@@ -0,0 +1,236 @@
+"use server"
+
+import { oracleKnex } from '@/lib/oracle-db/db'
+
+// CPVW_WAB_QUST_LIST_VIEW 테이블 데이터 타입 (실제 테이블 구조에 맞게 조정 필요)
+export interface CPVWWabQustListView {
+ [key: string]: string | number | Date | null | undefined
+}
+
+// 테스트 환경용 폴백 데이터 (실제 CPVW_WAB_QUST_LIST_VIEW 테이블 구조에 맞춤)
+const FALLBACK_TEST_DATA: CPVWWabQustListView[] = [
+ {
+ REG_NO: '1030',
+ INQ_TP: 'OC',
+ INQ_TP_DSC: '해외계약',
+ TIT: 'Contrack of Sale',
+ REQ_DGR: '2',
+ REQR_NM: '김원식',
+ REQ_DT: '20130829',
+ REVIEW_TERM_DT: '20130902',
+ RVWR_NM: '김미정',
+ CNFMR_NM: '안한진',
+ APPR_NM: '염정훈',
+ PRGS_STAT: 'E',
+ PRGS_STAT_DSC: '검토중',
+ REGR_DPTCD: 'D602058000',
+ REGR_DEPTNM: '구매1팀(사외계약)'
+ },
+ {
+ REG_NO: '1076',
+ INQ_TP: 'IC',
+ INQ_TP_DSC: '국내계약',
+ TIT: 'CAISSON PIPE 복관 계약서 검토 요청件',
+ REQ_DGR: '1',
+ REQR_NM: '서권환',
+ REQ_DT: '20130821',
+ REVIEW_TERM_DT: '20130826',
+ RVWR_NM: '이택준',
+ CNFMR_NM: '이택준',
+ APPR_NM: '전상용',
+ PRGS_STAT: 'E',
+ PRGS_STAT_DSC: '완료',
+ REGR_DPTCD: 'D602058000',
+ REGR_DEPTNM: '구매1팀(사외계약)'
+ },
+ {
+ REG_NO: '1100',
+ INQ_TP: 'IC',
+ INQ_TP_DSC: '국내계약',
+ TIT: '(7102) HVAC 작업계약',
+ REQ_DGR: '1',
+ REQR_NM: '신동동',
+ REQ_DT: '20130826',
+ REVIEW_TERM_DT: '20130829',
+ RVWR_NM: '이두리',
+ CNFMR_NM: '이두리',
+ APPR_NM: '전상용',
+ PRGS_STAT: 'E',
+ PRGS_STAT_DSC: '완료',
+ REGR_DPTCD: 'D602058000',
+ REGR_DEPTNM: '구매1팀(사외계약)'
+ },
+ {
+ REG_NO: '1105',
+ INQ_TP: 'IC',
+ INQ_TP_DSC: '국내계약',
+ TIT: 'Plate 가공계약서 검토 요청건',
+ REQ_DGR: '1',
+ REQR_NM: '서권환',
+ REQ_DT: '20130826',
+ REVIEW_TERM_DT: '20130829',
+ RVWR_NM: '백영국',
+ CNFMR_NM: '백영국',
+ APPR_NM: '전상용',
+ PRGS_STAT: 'E',
+ PRGS_STAT_DSC: '완료',
+ REGR_DPTCD: 'D602058000',
+ REGR_DEPTNM: '구매1팀(사외계약)'
+ },
+ {
+ REG_NO: '1106',
+ INQ_TP: 'IC',
+ INQ_TP_DSC: '국내계약',
+ TIT: 'SHELL FLNG, V-BRACE 제작 계약서 검토件',
+ REQ_DGR: '1',
+ REQR_NM: '성기승',
+ REQ_DT: '20130826',
+ REVIEW_TERM_DT: '20130830',
+ RVWR_NM: '이두리',
+ CNFMR_NM: '이두리',
+ APPR_NM: '전상용',
+ PRGS_STAT: 'E',
+ PRGS_STAT_DSC: '완료',
+ REGR_DPTCD: 'D602058000',
+ REGR_DEPTNM: '구매1팀(사외계약)'
+ }
+]
+
+const normalizeOracleRows = (rows: Array<Record<string, unknown>>): CPVWWabQustListView[] => {
+ return rows.map((item) => {
+ const convertedItem: CPVWWabQustListView = {}
+ 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
+ })
+}
+
+/**
+ * CPVW_WAB_QUST_LIST_VIEW 테이블 전체 조회
+ * @returns 테이블 데이터 배열
+ */
+export async function getCPVWWabQustListViewData(): Promise<{
+ success: boolean
+ data: CPVWWabQustListView[]
+ error?: string
+ isUsingFallback?: boolean
+}> {
+ try {
+ console.log('📋 [getCPVWWabQustListViewData] CPVW_WAB_QUST_LIST_VIEW 테이블 조회 시작...')
+
+ const result = await oracleKnex.raw(`
+ SELECT *
+ FROM CPVW_WAB_QUST_LIST_VIEW
+ WHERE ROWNUM < 100
+ ORDER BY 1
+ `)
+
+ // Oracle raw query의 결과는 rows 배열에 들어있음
+ const rows = (result.rows || result) as Array<Record<string, unknown>>
+
+ console.log(`✅ [getCPVWWabQustListViewData] 조회 성공 - ${rows.length}건`)
+
+ // 데이터 타입 변환 (필요에 따라 조정)
+ const cleanedResult = normalizeOracleRows(rows)
+
+ return {
+ success: true,
+ data: cleanedResult,
+ isUsingFallback: false
+ }
+ } catch (error) {
+ console.error('❌ [getCPVWWabQustListViewData] 오류:', error)
+ console.log('🔄 [getCPVWWabQustListViewData] 폴백 테스트 데이터 사용')
+ return {
+ success: true,
+ data: FALLBACK_TEST_DATA,
+ isUsingFallback: true
+ }
+ }
+}
+
+export async function getCPVWWabQustListViewByRegNo(regNo: string): Promise<{
+ success: boolean
+ data?: CPVWWabQustListView
+ error?: string
+ isUsingFallback?: boolean
+}> {
+ if (!regNo) {
+ return {
+ success: false,
+ error: 'REG_NO는 필수입니다.'
+ }
+ }
+
+ try {
+ console.log(`[getCPVWWabQustListViewByRegNo] REG_NO=${regNo} 조회`)
+ const result = await oracleKnex.raw(
+ `
+ SELECT *
+ FROM CPVW_WAB_QUST_LIST_VIEW
+ WHERE REG_NO = :regNo
+ `,
+ { regNo }
+ )
+
+ const rows = (result.rows || result) as Array<Record<string, unknown>>
+ const cleanedResult = normalizeOracleRows(rows)
+
+ if (cleanedResult.length === 0) {
+ // 데이터가 없을 때 폴백 테스트 데이터에서 찾기
+ console.log(`[getCPVWWabQustListViewByRegNo] 데이터 없음, 폴백 테스트 데이터에서 검색: REG_NO=${regNo}`)
+ const fallbackData = FALLBACK_TEST_DATA.find(item =>
+ String(item.REG_NO) === String(regNo)
+ )
+
+ if (fallbackData) {
+ console.log(`[getCPVWWabQustListViewByRegNo] 폴백 테스트 데이터에서 찾음: REG_NO=${regNo}`)
+ return {
+ success: true,
+ data: fallbackData,
+ isUsingFallback: true
+ }
+ }
+
+ return {
+ success: false,
+ error: '해당 REG_NO에 대한 데이터가 없습니다.'
+ }
+ }
+
+ return {
+ success: true,
+ data: cleanedResult[0],
+ isUsingFallback: false
+ }
+ } catch (error) {
+ console.error('[getCPVWWabQustListViewByRegNo] 오류:', error)
+ console.log(`[getCPVWWabQustListViewByRegNo] 폴백 테스트 데이터에서 검색: REG_NO=${regNo}`)
+
+ // 오류 발생 시 폴백 테스트 데이터에서 찾기
+ const fallbackData = FALLBACK_TEST_DATA.find(item =>
+ String(item.REG_NO) === String(regNo)
+ )
+
+ if (fallbackData) {
+ console.log(`[getCPVWWabQustListViewByRegNo] 폴백 테스트 데이터에서 찾음: REG_NO=${regNo}`)
+ return {
+ success: true,
+ data: fallbackData,
+ isUsingFallback: true
+ }
+ }
+
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : 'REG_NO 조회 중 오류가 발생했습니다.'
+ }
+ }
+}
diff --git a/lib/basic-contract/service.ts b/lib/basic-contract/service.ts
index 6f4e5d53..12278c54 100644
--- a/lib/basic-contract/service.ts
+++ b/lib/basic-contract/service.ts
@@ -2862,6 +2862,10 @@ export async function requestLegalReviewAction(
}
}
+// ⚠️ SSLVW(법무관리시스템) PRGS_STAT_DSC 문자열을 그대로 저장하는 함수입니다.
+// - 상태 텍스트 및 완료 여부는 외부 시스템에 의존하므로 신뢰도가 100%는 아니고,
+// - 여기에서 관리하는 값들은 UI 표시/참고용으로만 사용해야 합니다.
+// - 최종 승인 차단 등 핵심 비즈니스 로직에서는 SSLVW 쪽 완료 시간을 직접 신뢰하지 않습니다.
const persistLegalReviewStatus = async ({
contractId,
regNo,
@@ -2904,6 +2908,121 @@ const persistLegalReviewStatus = async ({
}
/**
+ * 준법문의 요청 서버 액션
+ */
+export async function requestComplianceInquiryAction(
+ contractIds: number[]
+): Promise<{ success: boolean; message: string }> {
+ const session = await getServerSession(authOptions)
+
+ if (!session?.user) {
+ return {
+ success: false,
+ message: "로그인이 필요합니다."
+ }
+ }
+
+ // 계약서 정보 조회
+ const contracts = await db
+ .select({
+ id: basicContractView.id,
+ complianceReviewRequestedAt: basicContractView.complianceReviewRequestedAt,
+ })
+ .from(basicContractView)
+ .where(inArray(basicContractView.id, contractIds))
+
+ if (contracts.length === 0) {
+ return {
+ success: false,
+ message: "선택된 계약서를 찾을 수 없습니다."
+ }
+ }
+
+ // 준법문의 요청 가능한 계약서 필터링 (이미 요청되지 않은 것만)
+ const eligibleContracts = contracts.filter(contract =>
+ !contract.complianceReviewRequestedAt
+ )
+
+ if (eligibleContracts.length === 0) {
+ return {
+ success: false,
+ message: "준법문의 요청 가능한 계약서가 없습니다."
+ }
+ }
+
+ const currentDate = new Date()
+
+ // 트랜잭션으로 처리
+ await db.transaction(async (tx) => {
+ for (const contract of eligibleContracts) {
+ await tx
+ .update(basicContract)
+ .set({
+ complianceReviewRequestedAt: currentDate,
+ updatedAt: currentDate,
+ })
+ .where(eq(basicContract.id, contract.id))
+ }
+ })
+
+ revalidateTag("basic-contracts")
+
+ return {
+ success: true,
+ message: `${eligibleContracts.length}건의 준법문의 요청이 완료되었습니다.`
+ }
+}
+
+/**
+ * 준법문의 상태 저장 (준법문의 전용 필드 사용)
+ */
+const persistComplianceReviewStatus = async ({
+ contractId,
+ regNo,
+ progressStatus,
+}: {
+ contractId: number
+ regNo: string
+ progressStatus: string
+}) => {
+ const now = new Date()
+
+ // 완료 상태 확인 (법무검토와 동일한 패턴)
+ // ⚠️ CPVW PRGS_STAT_DSC 문자열을 기반으로 한 best-effort 휴리스틱입니다.
+ // - 외부 시스템의 상태 텍스트에 의존하므로 신뢰도가 100%는 아니고,
+ // - 여기에서 설정하는 완료 시간(complianceReviewCompletedAt)은 UI 표시용으로만 사용해야 합니다.
+ // - 버튼 활성화, 서버 액션 차단, 필터 조건 등 핵심 비즈니스 로직에서는
+ // 이 값을 신뢰하지 않도록 합니다.
+ // 완료 상태 확인 (법무검토와 동일한 패턴)
+ const isCompleted = progressStatus && (
+ progressStatus.includes('완료') ||
+ progressStatus.includes('승인') ||
+ progressStatus.includes('종료')
+ )
+
+ await db.transaction(async (tx) => {
+ // 준법문의 상태 업데이트 (준법문의 전용 필드 사용)
+ const updateData: any = {
+ complianceReviewRegNo: regNo,
+ complianceReviewProgressStatus: progressStatus,
+ updatedAt: now,
+ }
+
+ // 완료 상태인 경우 완료일 설정
+ if (isCompleted) {
+ updateData.complianceReviewCompletedAt = now
+ }
+
+ await tx
+ .update(basicContract)
+ .set(updateData)
+ .where(eq(basicContract.id, contractId))
+ })
+
+ revalidateTag("basic-contracts")
+}
+
+/**
* SSLVW 데이터로부터 법무검토 상태 업데이트
* @param sslvwData 선택된 SSLVW 데이터 배열
* @param selectedContractIds 선택된 계약서 ID 배열
@@ -3033,6 +3152,137 @@ export async function updateLegalReviewStatusFromSSLVW(
}
}
+/**
+ * CPVW 데이터로부터 준법문의 상태 업데이트
+ * @param cpvwData 선택된 CPVW 데이터 배열
+ * @param selectedContractIds 선택된 계약서 ID 배열
+ * @returns 성공 여부 및 메시지
+ */
+export async function updateComplianceReviewStatusFromCPVW(
+ cpvwData: Array<{ REG_NO?: string; reg_no?: string; PRGS_STAT_DSC?: string; prgs_stat_dsc?: string; [key: string]: any }>,
+ selectedContractIds: number[]
+): Promise<{ success: boolean; message: string; updatedCount: number; errors: string[] }> {
+ try {
+ console.log(`[updateComplianceReviewStatusFromCPVW] CPVW 데이터로부터 준법문의 상태 업데이트 시작`)
+
+ if (!cpvwData || cpvwData.length === 0) {
+ return {
+ success: false,
+ message: 'CPVW 데이터가 없습니다.',
+ updatedCount: 0,
+ errors: []
+ }
+ }
+
+ if (!selectedContractIds || selectedContractIds.length === 0) {
+ return {
+ success: false,
+ message: '선택된 계약서가 없습니다.',
+ updatedCount: 0,
+ errors: []
+ }
+ }
+
+ if (selectedContractIds.length !== 1) {
+ return {
+ success: false,
+ message: '한 개의 계약서만 선택해 주세요.',
+ updatedCount: 0,
+ errors: []
+ }
+ }
+
+ if (cpvwData.length !== 1) {
+ return {
+ success: false,
+ message: '준법문의 시스템 데이터도 한 건만 선택해 주세요.',
+ updatedCount: 0,
+ errors: []
+ }
+ }
+
+ const contractId = selectedContractIds[0]
+ const cpvwItem = cpvwData[0]
+ const regNo = String(
+ cpvwItem.REG_NO ??
+ cpvwItem.reg_no ??
+ cpvwItem.RegNo ??
+ ''
+ ).trim()
+ const progressStatus = String(
+ cpvwItem.PRGS_STAT_DSC ??
+ cpvwItem.prgs_stat_dsc ??
+ cpvwItem.PrgsStatDsc ??
+ ''
+ ).trim()
+
+ if (!regNo) {
+ return {
+ success: false,
+ message: 'REG_NO 값을 찾을 수 없습니다.',
+ updatedCount: 0,
+ errors: []
+ }
+ }
+
+ if (!progressStatus) {
+ return {
+ success: false,
+ message: 'PRGS_STAT_DSC 값을 찾을 수 없습니다.',
+ updatedCount: 0,
+ errors: []
+ }
+ }
+
+ const contract = await db
+ .select({
+ id: basicContract.id,
+ complianceReviewRegNo: basicContract.complianceReviewRegNo,
+ })
+ .from(basicContract)
+ .where(eq(basicContract.id, contractId))
+ .limit(1)
+
+ if (!contract[0]) {
+ return {
+ success: false,
+ message: `계약서(${contractId})를 찾을 수 없습니다.`,
+ updatedCount: 0,
+ errors: []
+ }
+ }
+
+ if (contract[0].complianceReviewRegNo && contract[0].complianceReviewRegNo !== regNo) {
+ console.warn(`[updateComplianceReviewStatusFromCPVW] REG_NO가 변경됩니다: ${contract[0].complianceReviewRegNo} -> ${regNo}`)
+ }
+
+ // 준법문의 상태 업데이트
+ await persistComplianceReviewStatus({
+ contractId,
+ regNo,
+ progressStatus,
+ })
+
+ console.log(`[updateComplianceReviewStatusFromCPVW] 완료: 계약서 ${contractId}, REG_NO ${regNo}, 상태 ${progressStatus}`)
+
+ return {
+ success: true,
+ message: '준법문의 상태가 업데이트되었습니다.',
+ updatedCount: 1,
+ errors: []
+ }
+
+ } catch (error) {
+ console.error('[updateComplianceReviewStatusFromCPVW] 오류:', error)
+ return {
+ success: false,
+ message: '준법문의 상태 업데이트 중 오류가 발생했습니다.',
+ updatedCount: 0,
+ errors: [error instanceof Error ? error.message : '알 수 없는 오류']
+ }
+ }
+}
+
export async function refreshLegalReviewStatusFromOracle(contractId: number): Promise<{
success: boolean
message: string
@@ -3274,12 +3524,9 @@ export async function processBuyerSignatureAction(
}
}
- if (contractData.legalReviewRequestedAt && !contractData.legalReviewCompletedAt) {
- return {
- success: false,
- message: "법무검토가 완료되지 않았습니다."
- }
- }
+ // ⚠️ 법무검토 완료 여부는 SSLVW 상태/시간에 의존하므로
+ // 여기서는 legalReviewCompletedAt 기반으로 최종승인을 막지 않습니다.
+ // (법무 상태는 UI에서 참고 정보로만 사용)
// 파일 저장 로직 (기존 파일 덮어쓰기)
const saveResult = await saveBuffer({
@@ -3373,9 +3620,9 @@ export async function prepareFinalApprovalAction(
if (contract.completedAt !== null || !contract.signedFilePath) {
return false
}
- if (contract.legalReviewRequestedAt && !contract.legalReviewCompletedAt) {
- return false
- }
+ // ⚠️ 법무검토 완료 여부는 SSLVW 상태/시간에 의존하므로
+ // 여기서는 legalReviewCompletedAt 기반으로 필터링하지 않습니다.
+ // (법무 상태는 UI에서 참고 정보로만 사용)
return true
})
@@ -3949,6 +4196,8 @@ export async function saveGtcDocumentAction({
buyerSignedAt: null,
legalReviewRequestedAt: null,
legalReviewCompletedAt: null,
+ complianceReviewRequestedAt: null,
+ complianceReviewCompletedAt: null,
updatedAt: new Date()
})
.where(eq(basicContract.id, documentId))
diff --git a/lib/basic-contract/sslvw-service.ts b/lib/basic-contract/sslvw-service.ts
index 38ecb67d..08b43f82 100644
--- a/lib/basic-contract/sslvw-service.ts
+++ b/lib/basic-contract/sslvw-service.ts
@@ -10,18 +10,89 @@ export interface SSLVWPurInqReq {
// 테스트 환경용 폴백 데이터
const FALLBACK_TEST_DATA: SSLVWPurInqReq[] = [
{
- id: 1,
- request_number: 'REQ001',
- status: 'PENDING',
- created_date: new Date('2025-01-01'),
- description: '테스트 요청 1'
+ REG_NO: '1030',
+ INQ_TP: 'OC',
+ INQ_TP_DSC: '해외계약',
+ TIT: 'Contrack of Sale',
+ REQ_DGR: '2',
+ REQR_NM: '김원식',
+ REQ_DT: '20130829',
+ REVIEW_TERM_DT: '20130902',
+ RVWR_NM: '김미정',
+ CNFMR_NM: '안한진',
+ APPR_NM: '염정훈',
+ PRGS_STAT: 'E',
+ PRGS_STAT_DSC: '검토중이라고',
+ REGR_DPTCD: 'D602058000',
+ REGR_DEPTNM: '구매1팀(사외계약)'
},
{
- id: 2,
- request_number: 'REQ002',
- status: 'APPROVED',
- created_date: new Date('2025-01-02'),
- description: '테스트 요청 2'
+ REG_NO: '1076',
+ INQ_TP: 'IC',
+ INQ_TP_DSC: '국내계약',
+ TIT: 'CAISSON PIPE 복관 계약서 검토 요청件',
+ REQ_DGR: '1',
+ REQR_NM: '서권환',
+ REQ_DT: '20130821',
+ REVIEW_TERM_DT: '20130826',
+ RVWR_NM: '이택준',
+ CNFMR_NM: '이택준',
+ APPR_NM: '전상용',
+ PRGS_STAT: 'E',
+ PRGS_STAT_DSC: '완료',
+ REGR_DPTCD: 'D602058000',
+ REGR_DEPTNM: '구매1팀(사외계약)'
+ },
+ {
+ REG_NO: '1100',
+ INQ_TP: 'IC',
+ INQ_TP_DSC: '국내계약',
+ TIT: '(7102) HVAC 작업계약',
+ REQ_DGR: '1',
+ REQR_NM: '신동동',
+ REQ_DT: '20130826',
+ REVIEW_TERM_DT: '20130829',
+ RVWR_NM: '이두리',
+ CNFMR_NM: '이두리',
+ APPR_NM: '전상용',
+ PRGS_STAT: 'E',
+ PRGS_STAT_DSC: '완료',
+ REGR_DPTCD: 'D602058000',
+ REGR_DEPTNM: '구매1팀(사외계약)'
+ },
+ {
+ REG_NO: '1105',
+ INQ_TP: 'IC',
+ INQ_TP_DSC: '국내계약',
+ TIT: 'Plate 가공계약서 검토 요청건',
+ REQ_DGR: '1',
+ REQR_NM: '서권환',
+ REQ_DT: '20130826',
+ REVIEW_TERM_DT: '20130829',
+ RVWR_NM: '백영국',
+ CNFMR_NM: '백영국',
+ APPR_NM: '전상용',
+ PRGS_STAT: 'E',
+ PRGS_STAT_DSC: '완료',
+ REGR_DPTCD: 'D602058000',
+ REGR_DEPTNM: '구매1팀(사외계약)'
+ },
+ {
+ REG_NO: '1106',
+ INQ_TP: 'IC',
+ INQ_TP_DSC: '국내계약',
+ TIT: 'SHELL FLNG, V-BRACE 제작 계약서 검토件',
+ REQ_DGR: '1',
+ REQR_NM: '성기승',
+ REQ_DT: '20130826',
+ REVIEW_TERM_DT: '20130830',
+ RVWR_NM: '이두리',
+ CNFMR_NM: '이두리',
+ APPR_NM: '전상용',
+ PRGS_STAT: 'E',
+ PRGS_STAT_DSC: '완료',
+ REGR_DPTCD: 'D602058000',
+ REGR_DEPTNM: '구매1팀(사외계약)'
}
]
@@ -89,6 +160,7 @@ export async function getSSLVWPurInqReqByRegNo(regNo: string): Promise<{
success: boolean
data?: SSLVWPurInqReq
error?: string
+ isUsingFallback?: boolean
}> {
if (!regNo) {
return {
@@ -112,6 +184,21 @@ export async function getSSLVWPurInqReqByRegNo(regNo: string): Promise<{
const cleanedResult = normalizeOracleRows(rows)
if (cleanedResult.length === 0) {
+ // 데이터가 없을 때 폴백 테스트 데이터에서 찾기
+ console.log(`[getSSLVWPurInqReqByRegNo] 데이터 없음, 폴백 테스트 데이터에서 검색: REG_NO=${regNo}`)
+ const fallbackData = FALLBACK_TEST_DATA.find(item =>
+ String(item.REG_NO) === String(regNo)
+ )
+
+ if (fallbackData) {
+ console.log(`[getSSLVWPurInqReqByRegNo] 폴백 테스트 데이터에서 찾음: REG_NO=${regNo}`)
+ return {
+ success: true,
+ data: fallbackData,
+ isUsingFallback: true
+ }
+ }
+
return {
success: false,
error: '해당 REG_NO에 대한 데이터가 없습니다.'
@@ -120,10 +207,27 @@ export async function getSSLVWPurInqReqByRegNo(regNo: string): Promise<{
return {
success: true,
- data: cleanedResult[0]
+ data: cleanedResult[0],
+ isUsingFallback: false
}
} catch (error) {
console.error('[getSSLVWPurInqReqByRegNo] 오류:', error)
+ console.log(`[getSSLVWPurInqReqByRegNo] 폴백 테스트 데이터에서 검색: REG_NO=${regNo}`)
+
+ // 오류 발생 시 폴백 테스트 데이터에서 찾기
+ const fallbackData = FALLBACK_TEST_DATA.find(item =>
+ String(item.REG_NO) === String(regNo)
+ )
+
+ if (fallbackData) {
+ console.log(`[getSSLVWPurInqReqByRegNo] 폴백 테스트 데이터에서 찾음: REG_NO=${regNo}`)
+ return {
+ success: true,
+ data: fallbackData,
+ isUsingFallback: true
+ }
+ }
+
return {
success: false,
error: error instanceof Error ? error.message : 'REG_NO 조회 중 오류가 발생했습니다.'
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 575582cf..3e7caee1 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
@@ -18,9 +18,10 @@ import {
DialogTitle,
} from "@/components/ui/dialog"
import { Badge } from "@/components/ui/badge"
-import { prepareFinalApprovalAction, quickFinalApprovalAction, resendContractsAction, updateLegalReviewStatusFromSSLVW } from "../service"
+import { prepareFinalApprovalAction, quickFinalApprovalAction, resendContractsAction, updateLegalReviewStatusFromSSLVW, updateComplianceReviewStatusFromCPVW, requestComplianceInquiryAction } from "../service"
import { BasicContractSignDialog } from "../vendor-table/basic-contract-sign-dialog"
import { SSLVWPurInqReqDialog } from "@/components/common/legal/sslvw-pur-inq-req-dialog"
+import { CPVWWabQustListViewDialog } from "@/components/common/legal/cpvw-wab-qust-list-view-dialog"
import { prepareRedFlagResolutionApproval, requestRedFlagResolution } from "@/lib/compliance/red-flag-resolution"
import { useRouter } from "next/navigation"
import { useSession } from "next-auth/react"
@@ -81,24 +82,26 @@ export function BasicContractDetailTableToolbarActions({
if (contract.completedAt !== null || !contract.signedFilePath) {
return false;
}
- if (contract.legalReviewRequestedAt && !contract.legalReviewCompletedAt) {
- return false;
- }
+ // ⚠️ 법무/준법문의 완료 여부는 SSLVW/CPVW 상태 및 완료 시간에 의존하므로,
+ // 여기서는 legalReviewCompletedAt / complianceReviewCompletedAt 기반으로
+ // 최종 승인 버튼을 막지 않습니다. (상태/시간은 UI 참고용으로만 사용)
return true;
});
- // 법무검토 요청 가능 여부
- // 1. 협의 완료됨 (negotiationCompletedAt 있음) OR
- // 2. 협의 없음 (코멘트 없음, hasComments: false)
+ // 법무검토 요청 가능 여부 (준법서약 템플릿이 아닐 때만)
+ // 1. 협력업체 서명 완료 (vendorSignedAt 있음)
+ // 2. 협의 완료됨 (negotiationCompletedAt 있음) OR
+ // 3. 협의 없음 (코멘트 없음, hasComments: false)
// 협의 중 (negotiationCompletedAt 없고 코멘트 있음)은 불가
- const canRequestLegalReview = hasSelectedRows && selectedRows.some(row => {
+ const canRequestLegalReview = !isComplianceTemplate && hasSelectedRows && selectedRows.some(row => {
const contract = row.original;
- // 이미 법무검토 요청된 계약서는 제외
- if (contract.legalReviewRequestedAt) {
- return false;
- }
- // 이미 최종승인 완료된 계약서는 제외
- if (contract.completedAt) {
+
+ // 필수 조건 확인: 최종승인 미완료, 법무검토 미요청, 협력업체 서명 완료
+ if (
+ contract.legalReviewRequestedAt ||
+ contract.completedAt ||
+ !contract.vendorSignedAt
+ ) {
return false;
}
@@ -123,6 +126,35 @@ export function BasicContractDetailTableToolbarActions({
return false;
});
+ // 준법문의 버튼 활성화 가능 여부
+ // 1. 협력업체 서명 완료 (vendorSignedAt 있음)
+ // 2. 협의 완료 (negotiationCompletedAt 있음)
+ // 3. 레드플래그 해소됨 (redFlagResolutionData에서 resolved 상태)
+ // 4. 이미 준법문의 요청되지 않음 (complianceReviewRequestedAt 없음)
+ const canRequestComplianceInquiry = hasSelectedRows && selectedRows.some(row => {
+ const contract = row.original;
+
+ // 필수 조건 확인: 준법서약 템플릿, 최종승인 미완료, 협력업체 서명 완료, 협의 완료, 준법문의 미요청
+ if (
+ !isComplianceTemplate ||
+ contract.completedAt ||
+ !contract.vendorSignedAt ||
+ !contract.negotiationCompletedAt ||
+ contract.complianceReviewRequestedAt
+ ) {
+ return false;
+ }
+
+ // 레드플래그 해소 확인
+ const resolution = redFlagResolutionData[contract.id];
+ // 레드플래그가 있는 경우, 해소되어야 함
+ if (redFlagData[contract.id] === true && !resolution?.resolved) {
+ return false;
+ }
+
+ return true;
+ });
+
// 필터링된 계약서들 계산
const resendContracts = selectedRows.map(row => row.original)
@@ -394,6 +426,47 @@ export function BasicContractDetailTableToolbarActions({
}
}
+ // CPVW 데이터 선택 확인 핸들러
+ const handleCPVWConfirm = async (selectedCPVWData: any[]) => {
+ if (!selectedCPVWData || selectedCPVWData.length === 0) {
+ toast.error("선택된 데이터가 없습니다.")
+ return
+ }
+
+ if (selectedRows.length !== 1) {
+ toast.error("계약서 한 건을 선택해주세요.")
+ return
+ }
+
+ try {
+ setLoading(true)
+
+ // 선택된 계약서 ID들 추출
+ const selectedContractIds = selectedRows.map(row => row.original.id)
+
+ // 서버 액션 호출
+ const result = await updateComplianceReviewStatusFromCPVW(selectedCPVWData, selectedContractIds)
+
+ if (result.success) {
+ toast.success(result.message)
+ router.refresh()
+ table.toggleAllPageRowsSelected(false)
+ } else {
+ toast.error(result.message)
+ }
+
+ if (result.errors && result.errors.length > 0) {
+ toast.warning(`일부 처리 실패: ${result.errors.join(', ')}`)
+ }
+
+ } catch (error) {
+ console.error('CPVW 확인 처리 실패:', error)
+ toast.error('준법문의 상태 업데이트 중 오류가 발생했습니다.')
+ } finally {
+ setLoading(false)
+ }
+ }
+
// 빠른 승인 (서명 없이)
const confirmQuickApproval = async () => {
setLoading(true)
@@ -541,9 +614,26 @@ export function BasicContractDetailTableToolbarActions({
const complianceInquiryUrl = 'http://60.101.207.55/Inquiry/Write/InquiryWrite.aspx'
// 법무검토 요청 / 준법문의
- const handleRequestLegalReview = () => {
+ const handleRequestLegalReview = async () => {
if (isComplianceTemplate) {
- window.open(complianceInquiryUrl, '_blank', 'noopener,noreferrer')
+ // 준법문의: 요청일 기록 후 외부 URL 열기
+ const selectedContractIds = selectedRows.map(row => row.original.id)
+ try {
+ setLoading(true)
+ const result = await requestComplianceInquiryAction(selectedContractIds)
+ if (result.success) {
+ toast.success(result.message)
+ router.refresh()
+ window.open(complianceInquiryUrl, '_blank', 'noopener,noreferrer')
+ } else {
+ toast.error(result.message)
+ }
+ } catch (error) {
+ console.error('준법문의 요청 처리 실패:', error)
+ toast.error('준법문의 요청 중 오류가 발생했습니다.')
+ } finally {
+ setLoading(false)
+ }
return
}
setLegalReviewDialog(true)
@@ -617,31 +707,72 @@ export function BasicContractDetailTableToolbarActions({
</span>
</Button>
- {/* 법무검토 버튼 (SSLVW 데이터 조회) */}
- <SSLVWPurInqReqDialog
- onConfirm={handleSSLVWConfirm}
- requireSingleSelection
- triggerDisabled={selectedRows.length !== 1 || loading}
- triggerTitle={
- selectedRows.length !== 1
- ? "계약서 한 건을 선택해주세요"
- : undefined
- }
- />
+ {/* 법무검토 버튼 (SSLVW 데이터 조회) - 준법서약 템플릿이 아닐 때만 표시 */}
+ {!isComplianceTemplate && (
+ <SSLVWPurInqReqDialog
+ onConfirm={handleSSLVWConfirm}
+ requireSingleSelection
+ triggerDisabled={selectedRows.length !== 1 || loading}
+ triggerTitle={
+ selectedRows.length !== 1
+ ? "계약서 한 건을 선택해주세요"
+ : undefined
+ }
+ />
+ )}
+
+ {/* 준법문의 요청 데이터 조회 버튼 (준법서약 템플릿만) */}
+ {isComplianceTemplate && (
+ <CPVWWabQustListViewDialog
+ onConfirm={handleCPVWConfirm}
+ requireSingleSelection
+ triggerDisabled={selectedRows.length !== 1 || loading}
+ triggerTitle={
+ selectedRows.length !== 1
+ ? "계약서 한 건을 선택해주세요"
+ : undefined
+ }
+ />
+ )}
{/* 법무검토 요청 / 준법문의 버튼 */}
- <Button
- variant="outline"
- size="sm"
- onClick={handleRequestLegalReview}
- className="gap-2"
- title={isComplianceTemplate ? "준법문의 링크로 이동" : "법무검토 요청 링크 선택"}
- >
- <FileText className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">
- {isComplianceTemplate ? "준법문의" : "법무검토 요청"}
- </span>
- </Button>
+ {isComplianceTemplate ? (
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleRequestLegalReview}
+ className="gap-2"
+ disabled={!canRequestComplianceInquiry || loading}
+ title={
+ !canRequestComplianceInquiry
+ ? "협력업체 서명 완료, 협의 완료, 레드플래그 해소가 필요합니다"
+ : "준법문의 링크로 이동"
+ }
+ >
+ <FileText className="size-4" aria-hidden="true" />
+ <span className="hidden sm:inline">
+ 준법문의
+ </span>
+ </Button>
+ ) : (
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleRequestLegalReview}
+ className="gap-2"
+ disabled={!canRequestLegalReview || loading}
+ title={
+ !canRequestLegalReview
+ ? "협력업체 서명 완료 및 협의 완료가 필요합니다"
+ : "법무검토 요청 링크 선택"
+ }
+ >
+ <FileText className="size-4" aria-hidden="true" />
+ <span className="hidden sm:inline">
+ 법무검토 요청
+ </span>
+ </Button>
+ )}
{/* 최종승인 버튼 */}
<Button
diff --git a/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx b/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx
index aab808b8..de6ba1a9 100644
--- a/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx
+++ b/lib/basic-contract/status-detail/basic-contracts-detail-columns.tsx
@@ -553,8 +553,8 @@ export function getDetailColumns({
minSize: 130,
},
- // 법무검토 상태
- {
+ // 법무검토 상태 (준법서약 템플릿이 아닐 때만 표시)
+ ...(!isComplianceTemplate ? [{
accessorKey: "legalReviewStatus",
header: ({ column }) => (
<DataTableColumnHeaderSimple column={column} title="법무검토 상태" />
@@ -571,7 +571,30 @@ export function getDetailColumns({
return <div className="text-sm text-gray-400">-</div>
},
minSize: 140,
+ }] : []),
+
+ // 준법문의 상태 (준법서약 템플릿일 때만 표시)
+ ...(isComplianceTemplate ? [{
+ accessorKey: "complianceReviewStatus",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="준법문의 상태" />
+ ),
+ cell: ({ row }) => {
+ const status = row.getValue("complianceReviewStatus") as string | null
+
+ // PRGS_STAT_DSC 연동값 우선 표시
+ if (status) {
+ return <div className="text-sm text-gray-800">{status}</div>
+ }
+
+ // 동기화된 값이 없으면 빈 값 처리
+ return <div className="text-sm text-gray-400">-</div>
+ },
+ minSize: 140,
},
+ // Red Flag 컬럼들 (준법서약 템플릿일 때만 표시)
+ redFlagColumn,
+ redFlagResolutionColumn] : []),
// 계약완료일
{
@@ -659,17 +682,5 @@ export function getDetailColumns({
actionsColumn,
]
- // 준법서약 템플릿인 경우 Red Flag 컬럼과 해제 컬럼을 법무검토 상태 뒤에 추가
- if (isComplianceTemplate) {
- const legalReviewStatusIndex = baseColumns.findIndex((col) => {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- return (col as any).accessorKey === 'legalReviewStatus'
- })
-
- if (legalReviewStatusIndex !== -1) {
- baseColumns.splice(legalReviewStatusIndex + 1, 0, redFlagColumn, redFlagResolutionColumn)
- }
- }
-
return baseColumns
} \ No newline at end of file
diff --git a/types/table.d.ts b/types/table.d.ts
index d4053cf1..9fc96687 100644
--- a/types/table.d.ts
+++ b/types/table.d.ts
@@ -54,7 +54,7 @@ export type Filter<TData> = Prettify<
export interface DataTableRowAction<TData> {
row: Row<TData>
- type:"add_stage"|"specification_meeting"|"clone"|"viewVariables"|"variableSettings"|"addSubClause"|"createRevision"|"duplicate"|"dispose"|"restore"|"download_report"|"submit" |"general_evaluation"| "general_evaluation"|"esg_evaluation" |"schedule"| "view"| "upload" | "addInfo"| "view-series"|"log"| "tbeResult" | "requestInfo"| "esign-detail"| "responseDetail"|"signature"|"update" | "delete" | "user" | "pemission" | "invite" | "items" | "attachment" |"comments" | "open" | "select" | "files" | "vendor-submission"
+ type:"add_stage"|"specification_meeting"|"clone"|"viewVariables"|"variableSettings"|"addSubClause"|"createRevision"|"duplicate"|"dispose"|"restore"|"download_report"|"submit" |"general_evaluation"| "general_evaluation"|"esg_evaluation" |"schedule"| "view"| "upload" | "addInfo"| "view-series"|"log"| "tbeResult" | "requestInfo"| "esign-detail"| "responseDetail"|"signature"|"update" | "delete" | "user" | "pemission" | "invite" | "items" | "attachment" |"comments" | "open" | "select" | "files" | "vendor-submission" | "resend"
}
export interface QueryBuilderOpts {