diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-21 06:57:36 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-21 06:57:36 +0000 |
| commit | 02b1cf005cf3e1df64183d20ba42930eb2767a9f (patch) | |
| tree | e932c54d5260b0e6fda2b46be2a6ba1c3ee30434 /lib/basic-contract/vendor-table | |
| parent | d78378ecd7ceede1429359f8058c7a99ac34b1b7 (diff) | |
(대표님, 최겸) 설계메뉴추가, 작업사항 업데이트
설계메뉴 - 문서관리
설계메뉴 - 벤더 데이터
gtc 메뉴 업데이트
정보시스템 - 메뉴리스트 및 정보 업데이트
파일 라우트 업데이트
엑셀임포트 개선
기본계약 개선
벤더 가입과정 변경 및 개선
벤더 기본정보 - pq
돌체 오류 수정 및 개선
벤더 로그인 과정 이메일 오류 수정
Diffstat (limited to 'lib/basic-contract/vendor-table')
4 files changed, 404 insertions, 146 deletions
diff --git a/lib/basic-contract/vendor-table/basic-contract-columns.tsx b/lib/basic-contract/vendor-table/basic-contract-columns.tsx index c9e8da53..1b11285c 100644 --- a/lib/basic-contract/vendor-table/basic-contract-columns.tsx +++ b/lib/basic-contract/vendor-table/basic-contract-columns.tsx @@ -32,14 +32,65 @@ import { BasicContractView } from "@/db/schema" interface GetColumnsProps { setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<BasicContractView> | null>> + locale?: string + t: (key: string) => string // 번역 함수 } +// 기본 번역값들 (fallback) +const fallbackTranslations = { + ko: { + download: "다운로드", + selectAll: "전체 선택", + selectRow: "행 선택", + fileInfoMissing: "파일 정보가 없습니다.", + fileDownloadError: "파일 다운로드 중 오류가 발생했습니다.", + statusValues: { + PENDING: "서명대기", + COMPLETED: "서명완료" + } + }, + en: { + download: "Download", + selectAll: "Select all", + selectRow: "Select row", + fileInfoMissing: "File information is missing.", + fileDownloadError: "An error occurred while downloading the file.", + statusValues: { + PENDING: "Pending", + COMPLETED: "Completed" + } + } +}; + +// 안전한 번역 함수 +const safeTranslate = (t: (key: string) => string, key: string, locale: string = 'ko', fallback?: string): string => { + try { + const translated = t(key); + // 번역 키가 그대로 반환되는 경우 (번역 실패) fallback 사용 + if (translated === key && fallback) { + return fallback; + } + return translated || fallback || key; + } catch (error) { + console.warn(`Translation failed for key: ${key}`, error); + return fallback || key; + } +}; + /** * 파일 다운로드 함수 */ -const handleFileDownload = async (filePath: string | null, fileName: string | null) => { +const handleFileDownload = async ( + filePath: string | null, + fileName: string | null, + t: (key: string) => string, + locale: string = 'ko' +) => { + const fallback = fallbackTranslations[locale as keyof typeof fallbackTranslations] || fallbackTranslations.ko; + if (!filePath || !fileName) { - toast.error("파일 정보가 없습니다."); + const message = safeTranslate(t, "basicContracts.fileInfoMissing", locale, fallback.fileInfoMissing); + toast.error(message); return; } @@ -57,14 +108,17 @@ const handleFileDownload = async (filePath: string | null, fileName: string | nu } } catch (error) { console.error("파일 다운로드 오류:", error); - toast.error("파일 다운로드 중 오류가 발생했습니다."); + const message = safeTranslate(t, "basicContracts.fileDownloadError", locale, fallback.fileDownloadError); + toast.error(message); } }; /** * tanstack table 컬럼 정의 (중첩 헤더 버전) */ -export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<BasicContractView>[] { +export function getColumns({ setRowAction, locale = 'ko', t }: GetColumnsProps): ColumnDef<BasicContractView>[] { + const fallback = fallbackTranslations[locale as keyof typeof fallbackTranslations] || fallbackTranslations.ko; + // ---------------------------------------------------------------- // 1) select 컬럼 (체크박스) // ---------------------------------------------------------------- @@ -77,7 +131,7 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<BasicCo (table.getIsSomePageRowsSelected() && "indeterminate") } onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} - aria-label="Select all" + aria-label={safeTranslate(t, "basicContracts.selectAll", locale, fallback.selectAll)} className="translate-y-0.5" /> ), @@ -85,7 +139,7 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<BasicCo <Checkbox checked={row.getIsSelected()} onCheckedChange={(value) => row.toggleSelected(!!value)} - aria-label="Select row" + aria-label={safeTranslate(t, "basicContracts.selectRow", locale, fallback.selectRow)} className="translate-y-0.5" /> ), @@ -105,18 +159,19 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<BasicCo // PENDING 상태일 때는 원본 PDF 파일 (signedFilePath), COMPLETED일 때는 서명된 파일 (signedFilePath) const filePath = contract.signedFilePath; const fileName = contract.signedFileName; + const downloadText = safeTranslate(t, "basicContracts.download", locale, fallback.download); return ( <Button variant="ghost" size="icon" - onClick={() => handleFileDownload(filePath, fileName)} - title={`${fileName} 다운로드`} + onClick={() => handleFileDownload(filePath, fileName, t, locale)} + title={`${fileName} ${downloadText}`} className="hover:bg-muted" disabled={!filePath || !fileName} > <Paperclip className="h-4 w-4" /> - <span className="sr-only">다운로드</span> + <span className="sr-only">{downloadText}</span> </Button> ); }, @@ -124,7 +179,6 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<BasicCo enableSorting: false, } - // ---------------------------------------------------------------- // 4) 일반 컬럼들을 "그룹"별로 묶어 중첩 columns 생성 // ---------------------------------------------------------------- @@ -152,22 +206,28 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<BasicCo type: cfg.type, }, cell: ({ row, cell }) => { - // 날짜 형식 처리 + // 날짜 형식 처리 - 로케일 적용 if (cfg.id === "createdAt" || cfg.id === "updatedAt" || cfg.id === "completedAt") { const dateVal = cell.getValue() as Date - return formatDateTime(dateVal) + return formatDateTime(dateVal, locale) } - // Status 컬럼에 Badge 적용 + // Status 컬럼에 Badge 적용 - 다국어 적용 if (cfg.id === "status") { const status = row.getValue(cfg.id) as string const isPending = status === "PENDING" + const statusText = safeTranslate( + t, + `basicContracts.statusValues.${status}`, + locale, + fallback.statusValues[status as keyof typeof fallback.statusValues] || status + ); return ( <Badge variant={!isPending ? "default" : "secondary"} > - {status} + {statusText} </Badge> ) } @@ -175,8 +235,7 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<BasicCo // 나머지 컬럼은 그대로 값 표시 return row.getValue(cfg.id) ?? "" }, - minSize: 80, - + minSize: 80, } groupMap[groupName].push(childCol) @@ -194,10 +253,11 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<BasicCo // 그룹 없음 → 그냥 최상위 레벨 컬럼 nestedColumns.push(...colDefs) } else { - // 상위 컬럼 + // 상위 컬럼 - 그룹명 다국어 적용 + const translatedGroupName = t(`basicContracts.groups.${groupName}`) || groupName; nestedColumns.push({ id: groupName, - header: groupName, // "Basic Info", "Metadata" 등 + header: translatedGroupName, columns: colDefs, }) } diff --git a/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx b/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx index 7bffdac9..7d828a7e 100644 --- a/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx +++ b/lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx @@ -7,7 +7,6 @@ import { ScrollArea } from "@/components/ui/scroll-area"; import { formatDate } from "@/lib/utils"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; -import { BasicContractSignViewer } from "@/lib/basic-contract/viewer/basic-contract-sign-viewer"; import type { WebViewerInstance } from "@pdftron/webviewer"; import type { BasicContractView } from "@/db/schema"; import { @@ -19,45 +18,82 @@ import { FileText, User, AlertCircle, - Calendar + Calendar, + Loader2 } from "lucide-react"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; import { useRouter } from "next/navigation" +import { BasicContractSignViewer } from "../viewer/basic-contract-sign-viewer"; +import { getVendorAttachments } from "../service"; -// 수정된 props 인터페이스 interface BasicContractSignDialogProps { contracts: BasicContractView[]; onSuccess?: () => void; + hasSelectedRows?: boolean; + t: (key: string) => string; } -export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractSignDialogProps) { +export function BasicContractSignDialog({ + contracts, + onSuccess, + hasSelectedRows = false, + t +}: BasicContractSignDialogProps) { const [open, setOpen] = React.useState(false); const [selectedContract, setSelectedContract] = React.useState<BasicContractView | null>(null); const [instance, setInstance] = React.useState<null | WebViewerInstance>(null); const [searchTerm, setSearchTerm] = React.useState(""); const [isSubmitting, setIsSubmitting] = React.useState(false); + + // 추가된 state들 + const [additionalFiles, setAdditionalFiles] = React.useState<any[]>([]); + const [isLoadingAttachments, setIsLoadingAttachments] = React.useState(false); + const router = useRouter() + console.log(selectedContract,"selectedContract") + console.log(additionalFiles,"additionalFiles") + + // 버튼 비활성화 조건 + const isButtonDisabled = !hasSelectedRows || contracts.length === 0; + + // 비활성화 이유 텍스트 + const getDisabledReason = () => { + if (!hasSelectedRows) { + return t("basicContracts.toolbar.selectRows"); + } + if (contracts.length === 0) { + return t("basicContracts.toolbar.noPendingContracts"); + } + return ""; + }; + // 다이얼로그 열기/닫기 핸들러 const handleOpenChange = (isOpen: boolean) => { setOpen(isOpen); - // 다이얼로그가 열릴 때 첫 번째 계약서 자동 선택 - if (isOpen && contracts.length > 0 && !selectedContract) { - setSelectedContract(contracts[0]); - } - if (!isOpen) { + // 다이얼로그 닫을 때 상태 초기화 setSelectedContract(null); setSearchTerm(""); + setAdditionalFiles([]); // 추가 파일 상태 초기화 + // WebViewer 인스턴스 정리 + if (instance) { + try { + instance.UI.dispose(); + } catch (error) { + console.log("WebViewer dispose error:", error); + } + setInstance(null); + } } }; // 계약서 선택 핸들러 const handleSelectContract = (contract: BasicContractView) => { + console.log("계약서 선택:", contract.id, contract.templateName); setSelectedContract(contract); }; @@ -79,6 +115,40 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS } }, [open, contracts, selectedContract]); + // 추가 파일 가져오기 useEffect + React.useEffect(() => { + const fetchAdditionalFiles = async () => { + if (!selectedContract) { + setAdditionalFiles([]); + return; + } + + // "비밀유지 계약서"인 경우에만 추가 파일 가져오기 + if (selectedContract.templateName === "비밀유지 계약서") { + setIsLoadingAttachments(true); + try { + const result = await getVendorAttachments(selectedContract.vendorId); + if (result.success) { + setAdditionalFiles(result.data); + console.log("추가 파일 로드됨:", result.data); + } else { + console.error("Failed to fetch attachments:", result.error); + setAdditionalFiles([]); + } + } catch (error) { + console.error("Error fetching attachments:", error); + setAdditionalFiles([]); + } finally { + setIsLoadingAttachments(false); + } + } else { + setAdditionalFiles([]); + } + }; + + fetchAdditionalFiles(); + }, [selectedContract]); + // 서명 완료 핸들러 const completeSign = async () => { if (!instance || !selectedContract) return; @@ -89,29 +159,57 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS const doc = documentViewer.getDocument(); const xfdfString = await annotationManager.exportAnnotations(); + // 폼 필드 데이터 수집 + const fieldManager = annotationManager.getFieldManager(); + const fields = fieldManager.getFields(); + const formData: any = {}; + fields.forEach((field: any) => { + formData[field.name] = field.value; + }); + const data = await doc.getFileData({ xfdfString, downloadType: "pdf", }); // FormData 생성 및 파일 추가 - const formData = new FormData(); - formData.append('file', new Blob([data], { type: 'application/pdf' })); - formData.append('tableRowId', selectedContract.id.toString()); - formData.append('templateName', selectedContract.signedFileName || ''); + const submitFormData = new FormData(); + submitFormData.append('file', new Blob([data], { type: 'application/pdf' })); + submitFormData.append('tableRowId', selectedContract.id.toString()); + submitFormData.append('templateName', selectedContract.signedFileName || ''); + + // 폼 필드 데이터 추가 + if (Object.keys(formData).length > 0) { + submitFormData.append('formData', JSON.stringify(formData)); + } + + // 준법 템플릿인 경우 필수 필드 검증 + if (selectedContract.templateName?.includes('준법')) { + const requiredFields = ['compliance_agreement', 'legal_review', 'risk_assessment']; + const missingFields = requiredFields.filter(field => !formData[field]); + + if (missingFields.length > 0) { + toast.error("필수 준법 항목이 누락되었습니다.", { + description: `다음 항목을 완료해주세요: ${missingFields.join(', ')}`, + icon: <AlertCircle className="h-5 w-5 text-red-500" /> + }); + setIsSubmitting(false); + return; + } + } // API 호출 const response = await fetch('/api/upload/signed-contract', { method: 'POST', - body: formData, + body: submitFormData, next: { tags: ["basicContractView-vendor"] }, }); const result = await response.json(); if (result.result) { - toast.success("서명이 성공적으로 완료되었습니다.", { - description: "문서가 성공적으로 처리되었습니다.", + toast.success(t("basicContracts.messages.signSuccess"), { + description: t("basicContracts.messages.documentProcessed"), icon: <CheckCircle2 className="h-5 w-5 text-green-500" /> }); router.refresh(); @@ -120,22 +218,19 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS onSuccess(); } } else { - toast.error("서명 처리 중 오류가 발생했습니다.", { + toast.error(t("basicContracts.messages.signError"), { description: result.error, icon: <AlertCircle className="h-5 w-5 text-red-500" /> }); } } catch (error) { console.error("서명 완료 중 오류:", error); - toast.error("서명 처리 중 오류가 발생했습니다."); + toast.error(t("basicContracts.messages.signError")); } finally { setIsSubmitting(false); } }; - // 서명 대기중(PENDING) 계약서가 있는지 확인 - const hasPendingContracts = contracts.length > 0; - return ( <> {/* 서명 버튼 */} @@ -143,62 +238,67 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS variant="outline" size="sm" onClick={() => setOpen(true)} - disabled={!hasPendingContracts} - className="gap-2 transition-all hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200" + disabled={isButtonDisabled} + className="gap-2 transition-all hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200 disabled:opacity-50 disabled:cursor-not-allowed" > - <Upload className="size-4 text-blue-500" aria-hidden="true" /> + <Upload + className={`size-4 ${isButtonDisabled ? 'text-gray-400' : 'text-blue-500'}`} + aria-hidden="true" + /> <span className="hidden sm:inline flex items-center"> - 서명하기 - {contracts.length > 0 && ( + {t("basicContracts.toolbar.sign")} + {contracts.length > 0 && !isButtonDisabled && ( <Badge variant="secondary" className="ml-2 bg-blue-100 text-blue-700 hover:bg-blue-200"> {contracts.length} </Badge> )} + {isButtonDisabled && ( + <span className="ml-2 text-xs text-gray-400"> + ({getDisabledReason()}) + </span> + )} </span> </Button> - {/* 서명 다이얼로그 - 고정 높이 유지 */} + {/* 서명 다이얼로그 - 레이아웃 개선 */} <Dialog open={open} onOpenChange={handleOpenChange}> - <DialogContent className="max-w-5xl h-[650px] w-[90vw] p-0 overflow-hidden rounded-lg shadow-lg border border-gray-200"> - <DialogHeader className="p-6 bg-gradient-to-r from-blue-50 to-purple-50 border-b"> + <DialogContent className="max-w-7xl w-[95vw] h-[90vh] p-0 flex flex-col overflow-hidden" style={{width:'95vw', maxWidth:'95vw'}}> + {/* 고정 헤더 */} + <DialogHeader className="px-6 py-4 bg-gradient-to-r from-blue-50 to-purple-50 border-b flex-shrink-0"> <DialogTitle className="text-xl font-bold flex items-center text-gray-800"> <FileSignature className="mr-2 h-5 w-5 text-blue-500" /> - 기본계약서 및 관련문서 서명 + {t("basicContracts.dialog.title")} + {/* 추가 파일 로딩 표시 */} + {isLoadingAttachments && ( + <Loader2 className="ml-2 h-4 w-4 animate-spin text-blue-500" /> + )} </DialogTitle> </DialogHeader> - <div className="grid grid-cols-2 h-[calc(100%-4rem)] overflow-hidden"> - {/* 왼쪽 영역 - 계약서 목록 */} - <div className="col-span-1 border-r border-gray-200 bg-gray-50"> - <div className="p-4 border-b"> - <div className="relative mb-10"> - <div className="absolute inset-y-0 left-3.5 flex items-center pointer-events-none"> - <Search className="h-4 w-8 text-gray-400" /> + {/* 메인 컨텐츠 영역 - Flexbox 사용 */} + <div className="flex flex-1 min-h-0 overflow-hidden"> + {/* 왼쪽 영역 - 계약서 목록 (고정 너비) */} + <div className="w-80 border-r border-gray-200 bg-gray-50 flex flex-col flex-shrink-0"> + <div className="p-3 border-b flex-shrink-0"> + <div className="relative"> + <div className="absolute inset-y-0 left-2 flex items-center pointer-events-none"> + <Search className="h-4 w-4 text-gray-400" /> </div> <Input - placeholder="문서명 또는 요청자 검색" - className="bg-white" - style={{paddingLeft:25}} + placeholder={t("basicContracts.dialog.searchPlaceholder")} + className="bg-white pl-8 text-sm" value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} /> </div> - <Tabs defaultValue="all" className="w-full"> - <TabsList className="w-full"> - <TabsTrigger value="all" className="flex-1">전체 ({contracts.length})</TabsTrigger> - <TabsTrigger value="contracts" className="flex-1">계약서</TabsTrigger> - <TabsTrigger value="docs" className="flex-1">관련문서</TabsTrigger> - </TabsList> - </Tabs> </div> - <ScrollArea className="h-[calc(100%-6rem)]"> - <div className="p-3"> + <ScrollArea className="flex-1"> + <div className="p-2"> {filteredContracts.length === 0 ? ( - <div className="flex flex-col items-center justify-center h-40 text-center"> - <FileText className="h-12 w-12 text-gray-300 mb-2" /> - <p className="text-gray-500 font-medium">서명 요청된 문서가 없습니다.</p> - <p className="text-gray-400 text-sm mt-1">나중에 다시 확인해주세요.</p> + <div className="flex flex-col items-center justify-center h-32 text-center"> + <FileText className="h-8 w-8 text-gray-300 mb-2" /> + <p className="text-gray-500 text-sm font-medium">{t("basicContracts.dialog.noDocuments")}</p> </div> ) : ( <div className="space-y-2"> @@ -207,30 +307,38 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS key={contract.id} variant="outline" className={cn( - "w-full justify-start text-left h-auto p-3 bg-white hover:bg-blue-50 transition-colors", + "w-full justify-start text-left h-auto p-2 bg-white hover:bg-blue-50 transition-colors", "border border-gray-200 hover:border-blue-200 rounded-md", selectedContract?.id === contract.id && "border-blue-500 bg-blue-50 shadow-sm" )} onClick={() => handleSelectContract(contract)} > - <div className="flex flex-col w-full"> + <div className="flex flex-col w-full space-y-1"> + {/* 첫 번째 줄: 제목 + 상태 */} <div className="flex items-center justify-between w-full"> - <span className="font-semibold truncate text-gray-800 flex items-center"> - <FileText className="h-4 w-4 mr-2 text-blue-500" /> - {contract.templateName || '문서'} + <span className="font-medium text-xs truncate text-gray-800 flex items-center min-w-0"> + <FileText className="h-3 w-3 mr-1 text-blue-500 flex-shrink-0" /> + <span className="truncate">{contract.templateName || t("basicContracts.dialog.document")}</span> + {/* 비밀유지 계약서인 경우 표시 */} + {contract.templateName === "비밀유지 계약서" && ( + <Badge variant="outline" className="ml-1 bg-green-50 text-green-700 border-green-200 text-xs"> + NDA + </Badge> + )} </span> - <Badge variant="outline" className="bg-yellow-50 text-yellow-700 border-yellow-200"> - 대기중 + <Badge variant="outline" className="bg-yellow-50 text-yellow-700 border-yellow-200 text-xs ml-2 flex-shrink-0"> + {t("basicContracts.statusValues.PENDING")} </Badge> </div> - <Separator className="my-2 bg-gray-100" /> - <div className="grid grid-cols-2 gap-1 mt-1 text-xs text-gray-500"> - <div className="flex items-center"> - <User className="h-3 w-3 mr-1" /> - <span className="truncate">{contract.requestedByName || '알 수 없음'}</span> + + {/* 두 번째 줄: 사용자 + 날짜 */} + <div className="flex items-center justify-between text-xs text-gray-500"> + <div className="flex items-center min-w-0"> + <User className="h-3 w-3 mr-1 flex-shrink-0" /> + <span className="truncate">{contract.requestedByName || t("basicContracts.dialog.unknown")}</span> </div> - <div className="flex items-center justify-end"> - <Calendar className="h-3 w-3 mr-1" /> + <div className="flex items-center ml-2 flex-shrink-0"> + <Calendar className="h-3 w-3 mr-1 flex-shrink-0" /> <span>{formatDate(contract.createdAt)}</span> </div> </div> @@ -243,19 +351,32 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS </ScrollArea> </div> - {/* 오른쪽 영역 - 문서 뷰어 */} - <div className="col-span-1 bg-white flex flex-col h-full"> + {/* 오른쪽 영역 - 문서 뷰어 (확장 가능) */} + <div className="flex-1 bg-white flex flex-col min-w-0"> {selectedContract ? ( <> - <div className="p-3 border-b bg-gray-50"> + {/* 뷰어 헤더 */} + <div className="p-4 border-b bg-gray-50 flex-shrink-0"> <h3 className="font-semibold text-gray-800 flex items-center"> <FileText className="h-4 w-4 mr-2 text-blue-500" /> - {selectedContract.templateName || '문서'} + {selectedContract.templateName || t("basicContracts.dialog.document")} + {/* 준법 템플릿 표시 */} + {selectedContract.templateName?.includes('준법') && ( + <Badge variant="outline" className="ml-2 bg-amber-50 text-amber-700 border-amber-200"> + 준법 서류 + </Badge> + )} + {/* 비밀유지 계약서인 경우 추가 파일 수 표시 */} + {selectedContract.templateName === "비밀유지 계약서" && additionalFiles.length > 0 && ( + <Badge variant="outline" className="ml-2 bg-blue-50 text-blue-700 border-blue-200"> + 첨부파일 {additionalFiles.length}개 + </Badge> + )} </h3> - <div className="flex justify-between items-center mt-1 text-xs text-gray-500"> + <div className="flex justify-between items-center mt-2 text-sm text-gray-500"> <span className="flex items-center"> <User className="h-3 w-3 mr-1" /> - 요청자: {selectedContract.requestedByName || '알 수 없음'} + {t("basicContracts.dialog.requester")}: {selectedContract.requestedByName || t("basicContracts.dialog.unknown")} </span> <span className="flex items-center"> <Clock className="h-3 w-3 mr-1" /> @@ -263,19 +384,43 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS </span> </div> </div> - <div className="flex-grow overflow-hidden border-b"> + + {/* 뷰어 영역 - 남은 공간 모두 사용 */} + <div className="flex-1 min-h-0 overflow-hidden"> <BasicContractSignViewer + key={selectedContract.id} // key 추가로 컴포넌트 재생성 강제 contractId={selectedContract.id} filePath={selectedContract.signedFilePath || undefined} + templateName={selectedContract.templateName || ""} + additionalFiles={additionalFiles} // 추가 파일 전달 instance={instance} setInstance={setInstance} + t={t} /> </div> - <div className="p-3 flex justify-between items-center bg-gray-50"> - <p className="text-sm text-gray-600"> - <AlertCircle className="h-4 w-4 text-yellow-500 inline mr-1" /> - 서명 후에는 변경할 수 없습니다. - </p> + + {/* 고정 푸터 */} + <div className="p-4 flex justify-between items-center bg-gray-50 border-t flex-shrink-0"> + <div className="flex items-center space-x-4"> + <p className="text-sm text-gray-600 flex items-center"> + <AlertCircle className="h-4 w-4 text-yellow-500 mr-1" /> + {t("basicContracts.dialog.signWarning")} + </p> + {/* 준법 템플릿인 경우 추가 안내 */} + {selectedContract.templateName?.includes('준법') && ( + <p className="text-xs text-amber-600 flex items-center"> + <AlertCircle className="h-3 w-3 text-amber-500 mr-1" /> + 모든 준법 항목을 체크해주세요 + </p> + )} + {/* 비밀유지 계약서인 경우 추가 안내 */} + {selectedContract.templateName === "비밀유지 계약서" && additionalFiles.length > 0 && ( + <p className="text-xs text-blue-600 flex items-center"> + <FileText className="h-3 w-3 text-blue-500 mr-1" /> + 첨부 서류도 확인해주세요 + </p> + )} + </div> <Button className="gap-2 bg-blue-600 hover:bg-blue-700 transition-colors" onClick={completeSign} @@ -287,12 +432,12 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> </svg> - 처리 중... + {t("basicContracts.dialog.processing")} </> ) : ( <> <FileSignature className="h-4 w-4" /> - 서명 완료 + {t("basicContracts.dialog.completeSign")} </> )} </Button> @@ -303,9 +448,9 @@ export function BasicContractSignDialog({ contracts, onSuccess }: BasicContractS <div className="bg-blue-50 p-6 rounded-full mb-4"> <FileSignature className="h-12 w-12 text-blue-500" /> </div> - <h3 className="text-xl font-medium text-gray-800 mb-2">문서를 선택해주세요</h3> + <h3 className="text-xl font-medium text-gray-800 mb-2">{t("basicContracts.dialog.selectDocument")}</h3> <p className="text-gray-500 max-w-md"> - 왼쪽 목록에서 서명할 문서를 선택하면 여기에 문서 내용이 표시됩니다. + {t("basicContracts.dialog.selectDocumentDescription")} </p> </div> )} diff --git a/lib/basic-contract/vendor-table/basic-contract-table.tsx b/lib/basic-contract/vendor-table/basic-contract-table.tsx index 34e15ae3..f2575024 100644 --- a/lib/basic-contract/vendor-table/basic-contract-table.tsx +++ b/lib/basic-contract/vendor-table/basic-contract-table.tsx @@ -1,6 +1,8 @@ "use client"; import * as React from "react"; +import { useParams } from "next/navigation"; +import { useTranslation } from "react-i18next"; import { DataTable } from "@/components/data-table/data-table"; import { Button } from "@/components/ui/button"; import { Plus, Loader2 } from "lucide-react"; @@ -17,7 +19,6 @@ import { getBasicContracts, getBasicContractsByVendorId } from "../service"; import { BasicContractView } from "@/db/schema"; import { BasicContractTableToolbarActions } from "./basicContract-table-toolbar-actions"; - interface BasicTemplateTableProps { promises: Promise< [ @@ -26,44 +27,85 @@ interface BasicTemplateTableProps { > } - export function BasicContractsVendorTable({ promises }: BasicTemplateTableProps) { - - + const params = useParams(); + const lng = (params?.lng as string) || "ko"; + const { t, ready } = useTranslation(lng, "procurement"); + const [rowAction, setRowAction] = React.useState<DataTableRowAction<BasicContractView> | null>(null) - - + const [{ data, pageCount }] = React.use(promises) - // console.log(data) - - // 컬럼 설정 - 외부 파일에서 가져옴 + console.log(data,"data") + + // 안전한 번역 함수 (fallback 포함) + const safeT = React.useCallback((key: string, fallback: string) => { + if (!ready) return fallback; + const translated = t(key); + return translated === key ? fallback : translated; + }, [t, ready]); + + // 디버깅용 로그 (개발환경에서만) + React.useEffect(() => { + if (process.env.NODE_ENV === 'development') { + console.log('Translation ready:', ready); + console.log('Current language:', lng); + console.log('Template name translation:', t("basicContracts.templateName")); + console.log('Status PENDING translation:', t("basicContracts.statusValues.PENDING")); + } + }, [ready, lng, t]); + + // 컬럼 설정 - 번역이 준비된 후에만 생성 const columns = React.useMemo( - () => getColumns({ setRowAction }), - [setRowAction] + () => { + if (!ready) return []; // 번역이 준비되지 않으면 빈 배열 반환 + return getColumns({ setRowAction, locale: lng, t }); + }, + [setRowAction, lng, t, ready] ) - // config 기반으로 필터 필드 설정 - const advancedFilterFields: DataTableAdvancedFilterField<BasicContractView>[] = [ - { id: "templateName", label: "템플릿명", type: "text" }, - { - id: "status", label: "상태", type: "select", options: [ - { label: "서명대기", value: "PENDING" }, - { label: "서명완료", value: "COMPLETED" }, - ] - }, - { id: "userName", label: "요청자", type: "text" }, - { id: "createdAt", label: "생성일", type: "date" }, - { id: "updatedAt", label: "수정일", type: "date" }, - ]; + // config 기반으로 필터 필드 설정 - 안전한 번역 함수 사용 + const advancedFilterFields: DataTableAdvancedFilterField<BasicContractView>[] = React.useMemo(() => { + return [ + { + id: "templateName", + label: safeT("basicContracts.templateName", lng === 'ko' ? "템플릿명" : "Template Name"), + type: "text" + }, + { + id: "status", + label: safeT("basicContracts.status", lng === 'ko' ? "상태" : "Status"), + type: "select", + options: [ + { + label: safeT("basicContracts.statusValues.PENDING", lng === 'ko' ? "서명대기" : "Pending"), + value: "PENDING" + }, + { + label: safeT("basicContracts.statusValues.COMPLETED", lng === 'ko' ? "서명완료" : "Completed"), + value: "COMPLETED" + }, + ] + }, + { + id: "createdAt", + label: safeT("basicContracts.createdAt", lng === 'ko' ? "생성일" : "Created Date"), + type: "date" + }, + { + id: "updatedAt", + label: safeT("basicContracts.updatedAt", lng === 'ko' ? "수정일" : "Updated Date"), + type: "date" + }, + ]; + }, [safeT, lng]); const { table } = useDataTable({ data, columns, pageCount, - // filterFields, enablePinning: true, enableAdvancedFilter: true, initialState: { @@ -77,18 +119,14 @@ export function BasicContractsVendorTable({ promises }: BasicTemplateTableProps) return ( <> - <DataTable table={table}> <DataTableAdvancedToolbar table={table} filterFields={advancedFilterFields} > <BasicContractTableToolbarActions table={table} /> - </DataTableAdvancedToolbar> </DataTable> - </> - ); -}
\ No newline at end of file +}
\ No newline at end of file diff --git a/lib/basic-contract/vendor-table/basicContract-table-toolbar-actions.tsx b/lib/basic-contract/vendor-table/basicContract-table-toolbar-actions.tsx index 2e5e4471..1fc6fe6b 100644 --- a/lib/basic-contract/vendor-table/basicContract-table-toolbar-actions.tsx +++ b/lib/basic-contract/vendor-table/basicContract-table-toolbar-actions.tsx @@ -1,9 +1,10 @@ "use client" import * as React from "react" -import { type Task } from "@/db/schema/tasks" import { type Table } from "@tanstack/react-table" -import { Download, Upload } from "lucide-react" +import { Download } from "lucide-react" +import { useParams } from "next/navigation" +import { useTranslation } from "@/i18n/client" import { exportTableToExcel } from "@/lib/export" import { Button } from "@/components/ui/button" @@ -15,9 +16,19 @@ interface TemplateTableToolbarActionsProps { } export function BasicContractTableToolbarActions({ table }: TemplateTableToolbarActionsProps) { - // 파일 input을 숨기고, 버튼 클릭 시 참조해 클릭하는 방식 + const params = useParams() + const lng = (params?.lng as string) || "ko" + const { t, ready } = useTranslation(lng, "procurement") - const inPendingContracts = React.useMemo(() => { + // 안전한 번역 함수 + const safeT = React.useCallback((key: string, fallback: string) => { + if (!ready) return fallback; + const translated = t(key); + return translated === key ? fallback : translated; + }, [t, ready]); + + // PENDING 상태인 선택된 계약서들 + const pendingContracts = React.useMemo(() => { return table .getFilteredSelectedRowModel() .rows @@ -25,31 +36,35 @@ export function BasicContractTableToolbarActions({ table }: TemplateTableToolbar .filter(contract => contract.status === "PENDING"); }, [table.getFilteredSelectedRowModel().rows]); + // 선택된 행이 있는지 확인 + const hasSelectedRows = table.getFilteredSelectedRowModel().rows.length > 0; return ( <div className="flex items-center gap-2"> + {/* 서명 버튼 - 항상 표시하되 내부에서 조건 체크 */} + <BasicContractSignDialog + contracts={pendingContracts} + onSuccess={() => table.toggleAllRowsSelected(false)} + hasSelectedRows={hasSelectedRows} + t={safeT} + /> - {table.getFilteredSelectedRowModel().rows.length > 0 ? ( - <BasicContractSignDialog - contracts={inPendingContracts} - onSuccess={() => table.toggleAllRowsSelected(false)} - /> - ) : null} - - {/** 4) Export 버튼 */} + {/* Export 버튼 */} <Button variant="outline" size="sm" onClick={() => exportTableToExcel(table, { - filename: "basci-contract-requested-list", + filename: "basic-contract-requested-list", excludeColumns: ["select", "actions"], }) } className="gap-2" > <Download className="size-4" aria-hidden="true" /> - <span className="hidden sm:inline">Export</span> + <span className="hidden sm:inline"> + {safeT("basicContracts.toolbar.export", lng === 'ko' ? "내보내기" : "Export")} + </span> </Button> </div> ) |
