summaryrefslogtreecommitdiff
path: root/lib/basic-contract/viewer
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-11-17 08:26:41 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-11-17 08:26:41 +0000
commitd19cca70ad1689807192a8784efc3091bf677816 (patch)
treee3041a53dae9346eebada10e52c0d6e8d03f27ef /lib/basic-contract/viewer
parent4a077640becddf65ac2dc98451c5c83aa70108f8 (diff)
(임수민) 서명란 수정
Diffstat (limited to 'lib/basic-contract/viewer')
-rw-r--r--lib/basic-contract/viewer/basic-contract-sign-viewer.tsx247
1 files changed, 127 insertions, 120 deletions
diff --git a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx
index eeebae4e..4d98675b 100644
--- a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx
+++ b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx
@@ -9,7 +9,7 @@ import React, {
} from "react";
import { WebViewerInstance } from "@pdftron/webviewer";
import { Loader2, FileText, ClipboardList, AlertTriangle, FileSignature, Target, CheckCircle2, BookOpen } from "lucide-react";
-import { toast } from "sonner";
+import { useToast } from "@/hooks/use-toast";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -24,7 +24,7 @@ import {
import { getActiveSurveyTemplate, getVendorSignatureFile, type SurveyTemplateWithQuestions } from '../service';
import { useConditionalSurvey } from '../vendor-table/survey-conditional';
import { SurveyComponent } from './SurveyComponent';
-import { GtcClausesComponent } from './GtcClausesComponent';
+import { AgreementCommentList } from '../agreement-comments/agreement-comment-list';
import { getBuyerSignatureFileWithFallback } from "@/lib/shi-signature/buyer-signature";
interface FileInfo {
@@ -181,7 +181,7 @@ class AutoSignatureFieldDetector {
const fieldName = `signature_at_text_${Date.now()}_${fieldNames.length}`;
const signatureY = y + textHeight + 5;
- // 첫 번째 필드의 위치 정보만 저장 (서명란으로 이동 기능용)
+ // 첫 번째 필드의 위치 정보만 저장
if (fieldNames.length === 0) {
this.location = {
pageNumber,
@@ -355,7 +355,11 @@ const applyBuyerSignatureAutomatically = async (instance: WebViewerInstance) =>
};
-function useAutoSignatureFields(instance: WebViewerInstance | null, mode: 'vendor' | 'buyer' = 'vendor') {
+function useAutoSignatureFields(
+ instance: WebViewerInstance | null,
+ mode: 'vendor' | 'buyer' = 'vendor',
+ toastFn?: ReturnType<typeof useToast>['toast']
+) {
const [signatureFields, setSignatureFields] = useState<string[]>([]);
const [isProcessing, setIsProcessing] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -431,7 +435,11 @@ function useAutoSignatureFields(instance: WebViewerInstance | null, mode: 'vendo
processedDocumentRef.current = documentId;
}
- toast.success(`📋 ${fieldNames.length}개의 기존 서명 필드를 확인했습니다.`);
+ if (toastFn) {
+ toastFn({
+ title: `📋 ${fieldNames.length}개의 기존 서명 필드를 확인했습니다.`
+ });
+ }
return;
}
@@ -455,23 +463,23 @@ function useAutoSignatureFields(instance: WebViewerInstance | null, mode: 'vendo
const hasTextBasedField = fields.some(field => field.startsWith('signature_at_text_'));
const hasSimpleField = fields.some(field => field.startsWith('simple_signature_'));
- if (hasTextBasedField) {
- const searchText = mode === 'buyer' ? '삼성중공업_서명란' : '협력업체_서명란';
- toast.success(`📝 "${searchText}" 위치에 서명 필드가 생성되었습니다.`, {
- description: "해당 텍스트 근처의 파란색 영역에서 서명해주세요.",
- icon: <FileSignature className="h-4 w-4 text-blue-500" />,
- duration: 5000
- });
- } else if (hasSimpleField) {
- const positionMessage = mode === 'buyer'
- ? "마지막 페이지 왼쪽 하단의 파란색 영역에서 서명해주세요."
- : "마지막 페이지 하단의 파란색 영역에서 서명해주세요.";
-
- toast.info("📝 기본 위치에 서명 필드가 생성되었습니다.", {
- description: `검색 텍스트를 찾을 수 없어 ${positionMessage}`,
- icon: <FileSignature className="h-4 w-4 text-blue-500" />,
- duration: 5000
- });
+ if (toastFn) {
+ if (hasTextBasedField) {
+ const searchText = mode === 'buyer' ? '삼성중공업_서명란' : '협력업체_서명란';
+ toastFn({
+ title: `📝 "${searchText}" 위치에 서명 필드가 생성되었습니다.`,
+ description: "해당 텍스트 근처의 파란색 영역에서 서명해주세요."
+ });
+ } else if (hasSimpleField) {
+ const positionMessage = mode === 'buyer'
+ ? "마지막 페이지 왼쪽 하단의 파란색 영역에서 서명해주세요."
+ : "마지막 페이지 하단의 파란색 영역에서 서명해주세요.";
+
+ toastFn({
+ title: "📝 기본 위치에 서명 필드가 생성되었습니다.",
+ description: `검색 텍스트를 찾을 수 없어 ${positionMessage}`
+ });
+ }
}
}
@@ -480,10 +488,12 @@ function useAutoSignatureFields(instance: WebViewerInstance | null, mode: 'vendo
const errorMessage = error instanceof Error ? error.message : "서명 필드 처리에 실패했습니다.";
setError(errorMessage);
- toast.info("수동 서명 모드", {
- description: "문서에서 서명할 위치를 직접 클릭해주세요.",
- icon: <FileSignature className="h-4 w-4 text-blue-500" />
- });
+ if (toastFn) {
+ toastFn({
+ title: "수동 서명 모드",
+ description: "문서에서 서명할 위치를 직접 클릭해주세요."
+ });
+ }
} finally {
setIsProcessing(false);
processingRef.current = false;
@@ -528,7 +538,7 @@ function useAutoSignatureFields(instance: WebViewerInstance | null, mode: 'vendo
processedDocumentRef.current = null;
handlerRef.current = null;
};
- }, [instance, mode]); // mode 의존성 추가
+ }, [instance, mode, toastFn]); // mode, toastFn 의존성 추가
// 컴포넌트 언마운트 시 정리
useEffect(() => {
@@ -550,53 +560,6 @@ function useAutoSignatureFields(instance: WebViewerInstance | null, mode: 'vendo
};
}
-// 서명란으로 이동하는 함수 추가
-const navigateToSignatureField = (
- instance: WebViewerInstance | null,
- location: SignatureFieldLocation | null
-) => {
- if (!instance || !location) {
- toast.error("서명란 위치를 찾을 수 없습니다.");
- return;
- }
-
- try {
- const { documentViewer, annotationManager } = instance.Core;
-
- // 1. 해당 페이지로 이동
- documentViewer.setCurrentPage(location.pageNumber);
-
- // 2. 서명 필드 위젯 찾기
- const annotations = annotationManager.getAnnotationsList();
- const signatureWidget = annotations.find(
- (annot: any) => annot instanceof instance.Core.Annotations.SignatureWidgetAnnotation &&
- annot.getField()?.name === location.fieldName
- );
-
- if (signatureWidget) {
- // 위젯이 있으면 선택하여 자동으로 해당 위치로 스크롤
- annotationManager.selectAnnotation(signatureWidget);
-
- // 추가로 스크롤 위치 조정
- setTimeout(() => {
- try {
- // annotation을 다시 선택하여 스크롤 보장
- annotationManager.selectAnnotation(signatureWidget);
- } catch (scrollError) {
- console.warn("스크롤 조정 실패:", scrollError);
- }
- }, 300);
-
- toast.success(`서명란으로 이동했습니다. (페이지 ${location.pageNumber})`);
- } else {
- // 위젯을 찾지 못한 경우 페이지만 이동
- toast.info(`페이지 ${location.pageNumber}로 이동했습니다.`);
- }
- } catch (error) {
- console.error("서명란으로 이동 실패:", error);
- toast.error("서명란으로 이동하는데 실패했습니다.");
- }
-};
// XFDF 기반 서명 감지
function useSignatureDetection(instance: WebViewerInstance | null, onSignatureComplete?: () => void) {
const [hasValidSignature, setHasValidSignature] = useState(false);
@@ -730,6 +693,7 @@ export function BasicContractSignViewer({
mode = 'vendor', // 기본값 vendor
t = (key: string) => key,
}: BasicContractSignViewerProps) {
+ const { toast } = useToast();
const [fileLoading, setFileLoading] = useState<boolean>(true);
const [activeTab, setActiveTab] = useState<string>("main");
@@ -768,7 +732,7 @@ export function BasicContractSignViewer({
hasSignatureFields,
error: autoSignError,
signatureLocation // 위치 정보 추가
- } = useAutoSignatureFields(webViewerInstance.current || instance, mode);
+ } = useAutoSignatureFields(webViewerInstance.current || instance, mode, toast);
const { hasValidSignature } = useSignatureDetection(webViewerInstance.current || instance, onSignatureComplete);
@@ -788,7 +752,7 @@ export function BasicContractSignViewer({
});
}
- // 구매자 모드일 때는 추가 파일, 설문조사, 조항 검토 탭 제외
+ // 구매자 모드일 때는 추가 파일, 설문조사, 협의 코멘트 탭 제외
if (mode === 'buyer') {
return files; // 메인 계약서 파일만 반환
}
@@ -814,7 +778,7 @@ export function BasicContractSignViewer({
if (isGTCTemplate) {
files.push({
path: "",
- name: "조항 검토",
+ name: "협의 코멘트",
type: "clauses",
});
}
@@ -1056,19 +1020,28 @@ export function BasicContractSignViewer({
if (showToast) {
setFileLoading(false);
- toast.error(errorMessage);
+ toast({
+ title: errorMessage,
+ variant: "destructive"
+ });
}
});
}).catch((error) => {
console.error("📛 WebViewer 초기화 실패:", error);
setFileLoading(false);
- toast.error("뷰어 초기화에 실패했습니다.");
+ toast({
+ title: "뷰어 초기화에 실패했습니다.",
+ variant: "destructive"
+ });
});
}).catch((error) => {
console.error("📛 WebViewer 모듈 로드 실패:", error);
setFileLoading(false);
- toast.error("뷰어 모듈을 불러오는데 실패했습니다.");
+ toast({
+ title: "뷰어 모듈을 불러오는데 실패했습니다.",
+ variant: "destructive"
+ });
});
};
@@ -1139,7 +1112,10 @@ export function BasicContractSignViewer({
msg = "";
}
}
- if (msg) toast.error(msg);
+ if (msg) toast({
+ title: msg,
+ variant: "destructive"
+ });
} finally {
setFileLoading(false);
}
@@ -1250,10 +1226,13 @@ export function BasicContractSignViewer({
const { documentViewer, annotationManager } = currentInstance.Core;
const doc = documentViewer.getDocument();
- if (!doc) {
- toast.error("문서가 로드되지 않았습니다.");
- return;
- }
+ if (!doc) {
+ toast({
+ title: "문서가 로드되지 않았습니다.",
+ variant: "destructive"
+ });
+ return;
+ }
const formData = await collectFormData(currentInstance);
@@ -1266,14 +1245,20 @@ export function BasicContractSignViewer({
// 구매자 모드일 때는 설문조사와 GTC 검증 건너뛰기
if (mode !== 'buyer') {
if (isComplianceTemplate && !surveyData.completed) {
- toast.error("준법 설문조사를 먼저 완료해주세요.");
+ toast({
+ title: "준법 설문조사를 먼저 완료해주세요.",
+ variant: "destructive"
+ });
setActiveTab('survey');
return;
}
if (isGTCTemplate && gtcCommentStatus.hasComments && !gtcCommentStatus.isComplete) {
- toast.error("GTC 조항에 미해결 코멘트가 있어 서명할 수 없습니다.");
- toast.info("모든 코멘트를 삭제하거나 협의를 완료한 후 서명해주세요.");
+ toast({
+ title: "미해결 코멘트가 있어 서명할 수 없습니다.",
+ description: "모든 코멘트를 삭제하거나 협의를 완료한 후 서명해주세요.",
+ variant: "destructive"
+ });
setActiveTab('clauses');
return;
}
@@ -1283,14 +1268,19 @@ export function BasicContractSignViewer({
await onSign(documentData, { formData, surveyData, signatureFields });
} else {
const actionText = mode === 'buyer' ? "최종승인" : "서명";
- toast.success(`계약서가 성공적으로 ${actionText}되었습니다.`);
+ toast({
+ title: `계약서가 성공적으로 ${actionText}되었습니다.`
+ });
}
handleClose();
} catch (error) {
console.error(`📛 ${mode === 'buyer' ? '최종승인' : '서명'} 저장 실패:`, error);
const actionText = mode === 'buyer' ? "최종승인" : "서명";
- toast.error(`${actionText}을 저장하는데 실패했습니다.`);
+ toast({
+ title: `${actionText}을 저장하는데 실패했습니다.`,
+ variant: "destructive"
+ });
}
};
@@ -1326,17 +1316,6 @@ export function BasicContractSignViewer({
<Target className="h-3 w-3 mr-1" />
{signatureFields.length}개 서명 필드 자동 생성됨 {mode === 'buyer' ? '(왼쪽 하단)' : ''}
</Badge>
- {signatureLocation && (
- <Button
- variant="ghost"
- size="sm"
- className="h-6 px-2 text-xs hover:bg-blue-50"
- onClick={() => navigateToSignatureField(currentInstance, signatureLocation)}
- >
- <Target className="h-3 w-3 mr-1" />
- 서명란으로 이동
- </Button>
- )}
</>
) : null}
@@ -1420,15 +1399,27 @@ export function BasicContractSignViewer({
<div
className={`absolute inset-0 p-3 ${activeTab === 'clauses' ? 'block' : 'hidden'}`}
>
- {/* GTC 조항 컴포넌트 */}
- <GtcClausesComponent
- contractId={contractId}
- onCommentStatusChange={(hasComments, commentCount, reviewStatus, isComplete) => {
- setGtcCommentStatus({ hasComments, commentCount, reviewStatus, isComplete });
- onGtcCommentStatusChange?.(hasComments, commentCount, reviewStatus, isComplete);
- }}
- t={t}
- />
+ {/* 새로운 협의 코멘트 리스트 */}
+ {contractId ? (
+ <div className="h-full">
+ <AgreementCommentList
+ basicContractId={contractId}
+ currentUserType={mode === 'vendor' ? 'Vendor' : 'SHI'}
+ readOnly={false}
+ onCommentCountChange={(count) => {
+ const hasComments = count > 0;
+ const reviewStatus = hasComments ? 'negotiating' : 'draft';
+ const isComplete = false;
+ setGtcCommentStatus({ hasComments, commentCount: count, reviewStatus, isComplete });
+ onGtcCommentStatusChange?.(hasComments, count, reviewStatus, isComplete);
+ }}
+ />
+ </div>
+ ) : (
+ <div className="flex items-center justify-center h-full text-gray-500">
+ <p>계약서 정보를 불러올 수 없습니다. (contractId: {String(contractId)})</p>
+ </div>
+ )}
</div>
<div
@@ -1511,7 +1502,7 @@ export function BasicContractSignViewer({
<span className="block mt-1 text-amber-600">📋 준법 설문조사를 먼저 완료해주세요.</span>
)}
{mode !== 'buyer' && isGTCTemplate && (
- <span className="block mt-1 text-blue-600">📋 GTC 조항을 검토하고 코멘트가 없는지 확인해주세요.</span>
+ <span className="block mt-1 text-blue-600">📋 협의 코멘트를 확인하고 모든 협의가 완료되었는지 확인해주세요.</span>
)}
{hasSignatureFields && (
<span className="block mt-1 text-green-600">
@@ -1525,17 +1516,17 @@ export function BasicContractSignViewer({
)}
{mode !== 'buyer' && isGTCTemplate && gtcCommentStatus.hasComments && (
<span className="block mt-1 text-red-600">
- ⚠️ GTC 조항에 {gtcCommentStatus.commentCount}개의 코멘트가 있어 서명할 수 없습니다.
+ ⚠️ {gtcCommentStatus.commentCount}개의 코멘트가 있어 서명할 수 없습니다.
</span>
)}
{mode !== 'buyer' && isGTCTemplate && gtcCommentStatus.hasComments && !gtcCommentStatus.isComplete && (
<span className="block mt-1 text-red-600">
- ⚠️ GTC 조항에 {gtcCommentStatus.commentCount}개의 미해결 코멘트가 있어 서명할 수 없습니다.
+ ⚠️ {gtcCommentStatus.commentCount}개의 미해결 코멘트가 있어 서명할 수 없습니다.
</span>
)}
{mode !== 'buyer' && isGTCTemplate && gtcCommentStatus.hasComments && gtcCommentStatus.isComplete && (
<span className="block mt-1 text-green-600">
- ✅ GTC 조항 협의가 완료되어 서명 가능합니다.
+ ✅ 협의가 완료되어 서명 가능합니다.
</span>
)}
</DialogDescription>
@@ -1603,12 +1594,28 @@ export function BasicContractSignViewer({
</div>
<div className={`absolute inset-0 p-3 ${activeTab === 'clauses' ? 'block' : 'hidden'}`}>
- {/* GTC 조항 컴포넌트 */}
- <GtcClausesComponent
- contractId={contractId}
- onCommentStatusChange={handleGtcCommentStatusChange} // 메모이제이션된 콜백 사용
- t={t}
- />
+ {/* 새로운 협의 코멘트 리스트 */}
+ {contractId ? (
+ <div className="h-full">
+ <AgreementCommentList
+ basicContractId={contractId}
+ currentUserType={mode === 'vendor' ? 'Vendor' : 'SHI'}
+ readOnly={false}
+ onCommentCountChange={(count) => {
+ handleGtcCommentStatusChange?.(
+ count > 0,
+ count,
+ count > 0 ? 'negotiating' : 'draft',
+ false
+ );
+ }}
+ />
+ </div>
+ ) : (
+ <div className="flex items-center justify-center h-full text-gray-500">
+ <p>계약서 정보를 불러올 수 없습니다. (contractId: {String(contractId)})</p>
+ </div>
+ )}
</div>
<div className={`absolute inset-0 ${activeTab !== 'survey' && activeTab !== 'clauses' ? 'block' : 'hidden'}`}>