summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-11-20 02:52:16 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-11-20 02:52:16 +0000
commit77cbcaf27c9de8b361a6c5a13f0eefb37fd0d0e5 (patch)
tree4ae3c46af089c1f23f8e2b1650ba59f5f34e5fc4 /lib
parent088a161f8852dd7566619baca93257c0ccd901b7 (diff)
(임수민) 기본계약서 서명 위치, 설문조사 수정
Diffstat (limited to 'lib')
-rw-r--r--lib/basic-contract/viewer/basic-contract-sign-viewer.tsx164
1 files changed, 129 insertions, 35 deletions
diff --git a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx
index 7f5fa027..adc735e9 100644
--- a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx
+++ b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx
@@ -697,6 +697,9 @@ export function BasicContractSignViewer({
}: BasicContractSignViewerProps) {
const { toast } = useToast();
+ // 🔽 추가
+ const signatureFieldsRef = useRef<string[]>([]);
+
const [fileLoading, setFileLoading] = useState<boolean>(true);
const [activeTab, setActiveTab] = useState<string>("main");
const [surveyData, setSurveyData] = useState<any>({});
@@ -822,6 +825,11 @@ export function BasicContractSignViewer({
setTimeout(() => cleanupHtmlStyle(), 100);
};
+ // 🔽 최신 서명 필드 이름들을 ref에 동기화
+ useEffect(() => {
+ signatureFieldsRef.current = signatureFields;
+ }, [signatureFields]);
+
useEffect(() => {
setShowDialog(isOpen);
@@ -960,33 +968,57 @@ export function BasicContractSignViewer({
documentViewer.addEventListener('documentLoaded', handleDocumentLoaded);
- // 구매자 모드가 아닐 때만 자동 서명 적용
- if (mode !== 'buyer') {
- annotationManager.addEventListener('annotationChanged', async (annotList, type) => {
- for (const annot of annotList) {
- const { fieldName, X, Y, Width, Height, PageNumber } = annot;
-
- if (type === "add" && annot.Subject === "Widget") {
- const signatureImage = await getVendorSignatureFile()
-
- const stamp = new Annotations.StampAnnotation();
- stamp.PageNumber = PageNumber;
- stamp.X = X;
- stamp.Y = Y;
- stamp.Width = Width;
- stamp.Height = Height;
-
- const dataUrl = signatureImage?.data?.dataUrl;
- if (dataUrl) {
- await stamp.setImageData(dataUrl);
- annot.sign(stamp);
- annot.setFieldFlag(WidgetFlags.READ_ONLY, true);
- }
-
+ // 🔁 서명 관련 annotation 제어
+ annotationManager.addEventListener('annotationChanged', async (annotList, type) => {
+ if (type !== 'add') return;
+
+ for (const annot of annotList) {
+ const isWidgetSignature =
+ annot instanceof Annotations.SignatureWidgetAnnotation;
+
+ // 1) 필드 없는(Signature) 서명은 전부 막기
+ // - WebViewer 기본 "어디나 한 번 찍는" 서명은 보통 Subject 가 "Signature" 로 들어옵니다.
+ // - 혹시 다르면 console.log(annot) 찍어서 Subject 를 맞춰주면 됩니다.
+ if (!isWidgetSignature) {
+ if (annot.Subject === 'Signature') {
+ // 지정된 서명란 외 서명 → 즉시 삭제
+ annotationManager.deleteAnnotation(annot, false);
}
+ continue;
}
- });
- }
+
+ // 2) 서명 위젯(필드)은 "우리가 생성한 필드"만 허용
+ const field = annot.getField && annot.getField();
+ const name = field?.name as string | undefined;
+ const allowed = signatureFieldsRef.current;
+
+ if (name && allowed.length > 0 && !allowed.includes(name)) {
+ // 우리가 만든 서명 필드가 아니면 막기
+ annotationManager.deleteAnnotation(annot, false);
+ continue;
+ }
+
+ // 3) 여기까지 통과한 경우 = 우리가 만든 서명 필드에 대한 서명
+ // → 기존 자동 서명(협력사 서명) 로직 유지
+ if (mode !== 'buyer') {
+ const signatureImage = await getVendorSignatureFile();
+ if (!signatureImage?.data?.dataUrl) continue;
+
+ const stamp = new Annotations.StampAnnotation();
+ stamp.PageNumber = annot.PageNumber;
+ stamp.X = annot.X;
+ stamp.Y = annot.Y;
+ stamp.Width = annot.Width;
+ stamp.Height = annot.Height;
+
+ await stamp.setImageData(signatureImage.data.dataUrl);
+
+ const { WidgetFlags } = Annotations;
+ annot.sign(stamp);
+ annot.setFieldFlag(WidgetFlags.READ_ONLY, true);
+ }
+ }
+ });
newInstance.UI.setMinZoomLevel('25%');
newInstance.UI.setMaxZoomLevel('400%');
@@ -1329,6 +1361,15 @@ export function BasicContractSignViewer({
{/* 구매자 모드에서는 탭 없이 단일 뷰어만 표시 */}
{allFiles.length > 1 && mode !== 'buyer' ? (
<Tabs value={activeTab} onValueChange={handleTabChange} className="h-full flex flex-col">
+ {/* 준법설문 미완료 알림 배너 */}
+ {isComplianceTemplate && !surveyData.completed && (
+ <div className="bg-amber-50 border-b-2 border-amber-400 px-4 py-2 flex items-center justify-center space-x-2">
+ <AlertTriangle className="h-4 w-4 text-amber-600" />
+ <span className="text-sm font-semibold text-amber-700">
+ ⚠️ 준법 설문조사를 먼저 완료해주세요. 서명을 진행하려면 "준법 설문조사" 탭을 클릭하세요.
+ </span>
+ </div>
+ )}
<div className="border-b bg-gray-50 px-3 py-2 flex-shrink-0">
<TabsList className="grid w-full h-8" style={{ gridTemplateColumns: `repeat(${allFiles.length}, 1fr)` }}>
{allFiles.map((file, index) => {
@@ -1344,19 +1385,41 @@ export function BasicContractSignViewer({
tabId = `file-${fileOnlyIndex}`;
}
+ const isSurveyTab = file.type === 'survey';
+ const isSurveyIncomplete = isSurveyTab && !surveyData.completed;
+
return (
- <TabsTrigger key={tabId} value={tabId} className="text-xs">
+ <TabsTrigger
+ key={tabId}
+ value={tabId}
+ className={`text-xs relative ${
+ isSurveyIncomplete
+ ? 'bg-amber-50 border-2 border-amber-400 shadow-md'
+ : isSurveyTab
+ ? 'bg-blue-50 border border-blue-300'
+ : ''
+ }`}
+ >
<div className="flex items-center space-x-1">
{file.type === 'survey' ? (
- <ClipboardList className="h-3 w-3" />
+ <ClipboardList className={`h-3 w-3 ${isSurveyIncomplete ? 'text-amber-600' : 'text-blue-600'}`} />
) : file.type === 'clauses' ? (
<BookOpen className="h-3 w-3" />
) : (
<FileText className="h-3 w-3" />
)}
- <span className="truncate">{file.name}</span>
- {file.type === 'survey' && surveyData.completed && (
- <Badge variant="secondary" className="ml-1 h-4 px-1 text-xs">완료</Badge>
+ <span className={`truncate font-medium ${isSurveyIncomplete ? 'text-amber-700' : ''}`}>
+ {file.name}
+ </span>
+ {isSurveyTab && !surveyData.completed && (
+ <Badge variant="destructive" className="ml-1 h-4 px-1.5 text-xs bg-amber-500 text-white animate-bounce">
+ 필수
+ </Badge>
+ )}
+ {isSurveyTab && surveyData.completed && (
+ <Badge variant="secondary" className="ml-1 h-4 px-1 text-xs bg-green-500 text-white">
+ 완료
+ </Badge>
)}
{file.type === 'clauses' && gtcCommentStatus.hasComments && (
<Badge variant="destructive" className="ml-1 h-4 px-1 text-xs">
@@ -1529,6 +1592,15 @@ export function BasicContractSignViewer({
{/* 구매자 모드에서는 탭 없이 단일 뷰어만 표시 */}
{allFiles.length > 1 && mode !== 'buyer' ? (
<Tabs value={activeTab} onValueChange={handleTabChange} className="h-full flex flex-col">
+ {/* 준법설문 미완료 알림 배너 */}
+ {isComplianceTemplate && !surveyData.completed && (
+ <div className="bg-amber-50 border-b-2 border-amber-400 px-4 py-2 flex items-center justify-center space-x-2">
+ <AlertTriangle className="h-4 w-4 text-amber-600" />
+ <span className="text-sm font-semibold text-amber-700">
+ ⚠️ 준법 설문조사를 먼저 완료해주세요. 서명을 진행하려면 "준법 설문조사" 탭을 클릭하세요.
+ </span>
+ </div>
+ )}
<div className="border-b bg-gray-50 px-3 py-2 flex-shrink-0">
<TabsList className="grid w-full h-8" style={{ gridTemplateColumns: `repeat(${allFiles.length}, 1fr)` }}>
{allFiles.map((file, index) => {
@@ -1544,19 +1616,41 @@ export function BasicContractSignViewer({
tabId = `file-${fileOnlyIndex}`;
}
+ const isSurveyTab = file.type === 'survey';
+ const isSurveyIncomplete = isSurveyTab && !surveyData.completed;
+
return (
- <TabsTrigger key={tabId} value={tabId} className="text-xs">
+ <TabsTrigger
+ key={tabId}
+ value={tabId}
+ className={`text-xs relative ${
+ isSurveyIncomplete
+ ? 'bg-amber-50 border-2 border-amber-400 shadow-md'
+ : isSurveyTab
+ ? 'bg-blue-50 border border-blue-300'
+ : ''
+ }`}
+ >
<div className="flex items-center space-x-1">
{file.type === 'survey' ? (
- <ClipboardList className="h-3 w-3" />
+ <ClipboardList className={`h-3 w-3 ${isSurveyIncomplete ? 'text-amber-600' : 'text-blue-600'}`} />
) : file.type === 'clauses' ? (
<BookOpen className="h-3 w-3" />
) : (
<FileText className="h-3 w-3" />
)}
- <span className="truncate">{file.name}</span>
- {file.type === 'survey' && surveyData.completed && (
- <Badge variant="secondary" className="ml-1 h-4 px-1 text-xs">완료</Badge>
+ <span className={`truncate font-medium ${isSurveyIncomplete ? 'text-amber-700' : ''}`}>
+ {file.name}
+ </span>
+ {isSurveyTab && !surveyData.completed && (
+ <Badge variant="destructive" className="ml-1 h-4 px-1.5 text-xs bg-amber-500 text-white animate-bounce">
+ 필수
+ </Badge>
+ )}
+ {isSurveyTab && surveyData.completed && (
+ <Badge variant="secondary" className="ml-1 h-4 px-1 text-xs bg-green-500 text-white">
+ 완료
+ </Badge>
)}
{file.type === 'clauses' && gtcCommentStatus.hasComments && (
<Badge variant="destructive" className="ml-1 h-4 px-1 text-xs">