summaryrefslogtreecommitdiff
path: root/lib/basic-contract/vendor-table
diff options
context:
space:
mode:
Diffstat (limited to 'lib/basic-contract/vendor-table')
-rw-r--r--lib/basic-contract/vendor-table/basic-contract-columns.tsx96
-rw-r--r--lib/basic-contract/vendor-table/basic-contract-sign-dialog.tsx313
-rw-r--r--lib/basic-contract/vendor-table/basic-contract-table.tsx98
-rw-r--r--lib/basic-contract/vendor-table/basicContract-table-toolbar-actions.tsx43
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>
)