summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/[lng]/engineering/(engineering)/document-list-ship/page.tsx5
-rw-r--r--app/[lng]/evcp/(evcp)/document-list-ship/page.tsx5
-rw-r--r--app/[lng]/partners/(partners)/document-list-ship/page.tsx5
-rw-r--r--components/information/information-button.tsx96
-rw-r--r--lib/basic-contract/viewer/AutoSignatureFieldDetector.tsx493
-rw-r--r--lib/basic-contract/viewer/basic-contract-sign-viewer.tsx391
6 files changed, 338 insertions, 657 deletions
diff --git a/app/[lng]/engineering/(engineering)/document-list-ship/page.tsx b/app/[lng]/engineering/(engineering)/document-list-ship/page.tsx
index 321ce909..e3915419 100644
--- a/app/[lng]/engineering/(engineering)/document-list-ship/page.tsx
+++ b/app/[lng]/engineering/(engineering)/document-list-ship/page.tsx
@@ -37,7 +37,7 @@ export default async function IndexPage(props: IndexPageProps) {
<h2 className="text-2xl font-bold tracking-tight">
문서 관리
</h2>
- <InformationButton pagePath="partners/document-list-ship" />
+
</div>
{/* <p className="text-muted-foreground">
소속 회사의 모든 도서/도면을 확인하고 관리합니다.
@@ -107,9 +107,12 @@ export default async function IndexPage(props: IndexPageProps) {
<Shell className="gap-2">
<div className="flex items-center justify-between space-y-2">
<div>
+ <div className="flex items-center gap-2">
<h2 className="text-2xl font-bold tracking-tight">
조선 Document Management
</h2>
+ <InformationButton pagePath="evcp/document-list-ship" />
+ </div>
<p className="text-muted-foreground">
</p>
diff --git a/app/[lng]/evcp/(evcp)/document-list-ship/page.tsx b/app/[lng]/evcp/(evcp)/document-list-ship/page.tsx
index 321ce909..822e7cd4 100644
--- a/app/[lng]/evcp/(evcp)/document-list-ship/page.tsx
+++ b/app/[lng]/evcp/(evcp)/document-list-ship/page.tsx
@@ -37,7 +37,6 @@ export default async function IndexPage(props: IndexPageProps) {
<h2 className="text-2xl font-bold tracking-tight">
문서 관리
</h2>
- <InformationButton pagePath="partners/document-list-ship" />
</div>
{/* <p className="text-muted-foreground">
소속 회사의 모든 도서/도면을 확인하고 관리합니다.
@@ -107,9 +106,13 @@ export default async function IndexPage(props: IndexPageProps) {
<Shell className="gap-2">
<div className="flex items-center justify-between space-y-2">
<div>
+ <div className="flex items-center gap-2">
<h2 className="text-2xl font-bold tracking-tight">
조선 Document Management
</h2>
+ <InformationButton pagePath="evcp/document-list-ship" />
+ </div>
+
<p className="text-muted-foreground">
</p>
diff --git a/app/[lng]/partners/(partners)/document-list-ship/page.tsx b/app/[lng]/partners/(partners)/document-list-ship/page.tsx
index ad3bf30d..c70a0c03 100644
--- a/app/[lng]/partners/(partners)/document-list-ship/page.tsx
+++ b/app/[lng]/partners/(partners)/document-list-ship/page.tsx
@@ -36,7 +36,6 @@ export default async function IndexPage(props: IndexPageProps) {
<h2 className="text-2xl font-bold tracking-tight">
문서 관리
</h2>
- <InformationButton pagePath="partners/document-list-ship" />
</div>
{/* <p className="text-muted-foreground">
소속 회사의 모든 도서/도면을 확인하고 관리합니다.
@@ -108,9 +107,13 @@ export default async function IndexPage(props: IndexPageProps) {
<Shell className="gap-2">
<div className="flex items-center justify-between space-y-2">
<div>
+ <div className="flex items-center gap-2">
<h2 className="text-2xl font-bold tracking-tight">
{vendorName} Document Management
</h2>
+ <InformationButton pagePath="partners/document-list-ship" />
+ </div>
+
<p className="text-muted-foreground">
</p>
diff --git a/components/information/information-button.tsx b/components/information/information-button.tsx
index 17f10502..69cbb106 100644
--- a/components/information/information-button.tsx
+++ b/components/information/information-button.tsx
@@ -50,61 +50,87 @@ export function InformationButton({
const [isNoticeViewDialogOpen, setIsNoticeViewDialogOpen] = useState(false)
const [dataLoaded, setDataLoaded] = useState(false)
const [isLoading, setIsLoading] = useState(false)
+ const [retryCount, setRetryCount] = useState(0)
- // 데이터 로드 함수 (단순화)
+ // 데이터 로드 함수
const loadData = React.useCallback(async () => {
- if (dataLoaded) return // 이미 로드되었으면 중복 방지
+ if (dataLoaded) return
setIsLoading(true)
try {
- // pagePath 정규화 (앞의 / 제거)
- const normalizedPath = pagePath.startsWith('/') ? pagePath.slice(1) : pagePath
-
- console.log('🔍 Information Button - 데이터 로딩:', {
- originalPath: pagePath,
- normalizedPath: normalizedPath,
- sessionUserId: session?.user?.id
- })
+ // 경로 정규화 - 더 안전한 방식
+ let normalizedPath = pagePath
+ if (normalizedPath.startsWith('/')) {
+ normalizedPath = normalizedPath.slice(1)
+ }
+ // 빈 문자열이면 기본값 설정
+ if (!normalizedPath) {
+ normalizedPath = 'home'
+ }
- // 병렬로 데이터 조회
- const [infoResult, noticesResult] = await Promise.all([
- getPageInformationDirect(normalizedPath),
- getPageNotices(normalizedPath)
- ])
+ // 약간의 지연 추가 (프로덕션에서 DB 연결 안정성)
+ if (retryCount > 0) {
+ await new Promise(resolve => setTimeout(resolve, 500 * retryCount))
+ }
- console.log('📊 조회 결과:', {
- infoResult: infoResult ? {
- id: infoResult.id,
- pagePath: infoResult.pagePath,
- pageName: infoResult.pageName,
- attachmentsCount: infoResult.attachments?.length || 0
- } : null,
- noticesCount: noticesResult.length
- })
+ // 순차적으로 데이터 조회 (프로덕션 안정성)
+ const infoResult = await getPageInformationDirect(normalizedPath)
+ const noticesResult = await getPageNotices(normalizedPath)
setInformation(infoResult)
setNotices(noticesResult)
setDataLoaded(true)
+ setRetryCount(0) // 성공시 재시도 횟수 리셋
- // 권한 확인
- if (session?.user?.id) {
- const hasPermission = await getEditPermissionDirect(normalizedPath, session.user.id)
- setHasEditPermission(hasPermission)
+ // 권한 확인 - 세션이 확실히 있을 때만
+ if (session?.user?.id && infoResult) {
+ try {
+ const hasPermission = await getEditPermissionDirect(normalizedPath, session.user.id)
+ setHasEditPermission(hasPermission)
+ } catch (permError) {
+ setHasEditPermission(false)
+ }
}
} catch (error) {
- console.error("데이터 로딩 중 오류:", error)
+ // 재시도 로직
+ if (retryCount < 2) {
+ setRetryCount(prev => prev + 1)
+ setIsLoading(false)
+ return
+ }
+
+ // 최대 재시도 후 기본값 설정
+ setInformation(null)
+ setNotices([])
+ setHasEditPermission(false)
+ setDataLoaded(true)
+ setRetryCount(0)
} finally {
setIsLoading(false)
}
- }, [pagePath, session?.user?.id, dataLoaded])
+ }, [pagePath, session?.user?.id, dataLoaded, retryCount])
+
+ // 세션이 준비되면 자동으로 데이터 로드
+ React.useEffect(() => {
+ if (isOpen && !dataLoaded && session !== undefined) {
+ loadData()
+ }
+ }, [isOpen, dataLoaded, session])
+
+ // 재시도 처리
+ React.useEffect(() => {
+ if (retryCount > 0 && retryCount <= 2) {
+ const timer = setTimeout(() => {
+ setDataLoaded(false) // 재시도를 위해 리셋
+ }, 500 * retryCount)
+ return () => clearTimeout(timer)
+ }
+ }, [retryCount])
// 다이얼로그 열기
const handleDialogOpen = (open: boolean) => {
setIsOpen(open)
-
- if (open && !dataLoaded) {
- loadData()
- }
+ // useEffect에서 데이터 로딩 처리하므로 여기서는 제거
}
// 편집 관련 핸들러
@@ -116,7 +142,7 @@ export function InformationButton({
setIsEditDialogOpen(false)
// 편집 후 데이터 다시 로드
setDataLoaded(false)
- loadData()
+ setRetryCount(0)
}
// 공지사항 클릭 핸들러
diff --git a/lib/basic-contract/viewer/AutoSignatureFieldDetector.tsx b/lib/basic-contract/viewer/AutoSignatureFieldDetector.tsx
deleted file mode 100644
index 7de8062c..00000000
--- a/lib/basic-contract/viewer/AutoSignatureFieldDetector.tsx
+++ /dev/null
@@ -1,493 +0,0 @@
-// PDF 텍스트 패턴 기반 자동 서명 필드 생성 시스템
-
-interface SignaturePattern {
- regex: RegExp;
- name: string;
- priority: number;
- offsetX?: number; // 텍스트로부터 X축 오프셋
- offsetY?: number; // 텍스트로부터 Y축 오프셋
- width?: number; // 서명 필드 너비
- height?: number; // 서명 필드 높이
- }
-
- interface DetectedSignatureLocation {
- pageIndex: number;
- text: string;
- rect: {
- x1: number;
- y1: number;
- x2: number;
- y2: number;
- };
- pattern: SignaturePattern;
- confidence: number;
- }
-
- class AutoSignatureFieldDetector {
- private instance: WebViewerInstance;
- private signaturePatterns: SignaturePattern[];
-
- constructor(instance: WebViewerInstance) {
- this.instance = instance;
- this.signaturePatterns = this.initializePatterns();
- }
-
- private initializePatterns(): SignaturePattern[] {
- return [
- // 한국어 패턴들 (우선순위 높음)
- {
- regex: /서명\s*[::]\s*[_\-\s]{3,}/gi,
- name: "한국어_서명_콜론",
- priority: 10,
- offsetX: 80, // "서명:" 텍스트 오른쪽으로 80px
- offsetY: -5, // 약간 위로
- width: 150,
- height: 40
- },
- {
- regex: /서명란\s*[_\-\s]{0,}/gi,
- name: "한국어_서명란",
- priority: 9,
- offsetX: 60,
- offsetY: -5,
- width: 150,
- height: 40
- },
- {
- regex: /서명\s*[_\-\s]{5,}/gi,
- name: "한국어_서명_라인",
- priority: 8,
- offsetX: 50,
- offsetY: -5,
- width: 150,
- height: 40
- },
- {
- regex: /(계약자|갑|을)\s*서명\s*[::]?\s*[_\-\s]{0,}/gi,
- name: "한국어_계약자_서명",
- priority: 9,
- offsetX: 100,
- offsetY: -5,
- width: 150,
- height: 40
- },
-
- // 영어 패턴들
- {
- regex: /signature\s*[::]\s*[_\-\s]{3,}/gi,
- name: "영어_signature_콜론",
- priority: 8,
- offsetX: 120,
- offsetY: -5,
- width: 150,
- height: 40
- },
- {
- regex: /sign\s+here\s*[::]?\s*[_\-\s]{0,}/gi,
- name: "영어_sign_here",
- priority: 9,
- offsetX: 100,
- offsetY: -5,
- width: 150,
- height: 40
- },
- {
- regex: /sign\s*[::]\s*[_\-\s]{3,}/gi,
- name: "영어_sign_콜론",
- priority: 7,
- offsetX: 60,
- offsetY: -5,
- width: 150,
- height: 40
- },
-
- // 날짜와 함께 나오는 패턴들
- {
- regex: /날짜\s*[::]\s*[_\-\s]{3,}.*?서명\s*[::]\s*[_\-\s]{3,}/gi,
- name: "날짜_서명_조합",
- priority: 10,
- offsetX: 0,
- offsetY: -5,
- width: 150,
- height: 40
- },
- {
- regex: /date\s*[::]\s*[_\-\s]{3,}.*?signature\s*[::]\s*[_\-\s]{3,}/gi,
- name: "date_signature_조합",
- priority: 10,
- offsetX: 0,
- offsetY: -5,
- width: 150,
- height: 40
- },
-
- // 일반적인 양식 패턴들
- {
- regex: /이름\s*[::]\s*[_\-\s]{5,}.*?서명\s*[::]\s*[_\-\s]{5,}/gi,
- name: "이름_서명_조합",
- priority: 8,
- offsetX: 0,
- offsetY: -5,
- width: 150,
- height: 40
- },
- {
- regex: /name\s*[::]\s*[_\-\s]{5,}.*?signature\s*[::]\s*[_\-\s]{5,}/gi,
- name: "name_signature_조합",
- priority: 8,
- offsetX: 0,
- offsetY: -5,
- width: 150,
- height: 40
- }
- ];
- }
-
- // 📄 메인 함수: 문서에서 서명 패턴 감지 및 필드 생성
- async detectAndCreateSignatureFields(): Promise<string[]> {
- console.log("🔍 자동 서명 필드 감지 시작...");
-
- try {
- const { Core } = this.instance;
- const { documentViewer } = Core;
-
- await Core.PDFNet.initialize();
- const doc = await documentViewer.getDocument().getPDFDoc();
-
- // 1. 기존 서명 필드 확인
- const existingFields = await this.getExistingSignatureFields(doc);
- console.log(`📊 기존 서명 필드: ${existingFields.length}개`);
-
- if (existingFields.length > 0) {
- console.log("✅ 기존 서명 필드가 있으므로 자동 생성 스킵");
- return existingFields.map(f => f.name);
- }
-
- // 2. 텍스트 패턴 기반 서명 위치 감지
- const detectedLocations = await this.detectSignatureLocations(doc);
- console.log(`🎯 감지된 서명 위치: ${detectedLocations.length}개`);
-
- // 3. 감지된 위치에 서명 필드 생성
- const createdFields: string[] = [];
- for (const location of detectedLocations) {
- try {
- const fieldName = await this.createSignatureFieldAtLocation(doc, location);
- createdFields.push(fieldName);
- console.log(`✅ 서명 필드 생성: ${fieldName}`);
- } catch (error) {
- console.error(`📛 서명 필드 생성 실패:`, error);
- }
- }
-
- // 4. 문서 업데이트
- if (createdFields.length > 0) {
- await documentViewer.refreshAll();
- await documentViewer.updateView();
- console.log(`🎉 총 ${createdFields.length}개 서명 필드 자동 생성 완료`);
- } else {
- console.warn("⚠️ 서명 패턴을 찾지 못했습니다. 기본 서명 필드 생성...");
- const defaultField = await this.createDefaultSignatureField(doc);
- createdFields.push(defaultField);
- }
-
- return createdFields;
-
- } catch (error) {
- console.error("📛 자동 서명 필드 생성 실패:", error);
- return [];
- }
- }
-
- // 기존 서명 필드 확인
- private async getExistingSignatureFields(doc: any): Promise<any[]> {
- const { Core } = this.instance;
- const fields = [];
-
- try {
- const pageCount = await doc.getPageCount();
-
- for (let i = 1; i <= pageCount; i++) {
- const page = await doc.getPage(i);
- const numAnnots = await page.getNumAnnots();
-
- for (let j = 0; j < numAnnots; j++) {
- const annot = await page.getAnnot(j);
- const annotType = await annot.getType();
-
- if (annotType === Core.PDFNet.Annot.Type.e_Widget) {
- const widget = await Core.PDFNet.WidgetAnnot.cast(annot);
- const field = await widget.getField();
- const fieldType = await field.getType();
-
- if (fieldType === Core.PDFNet.Field.Type.e_signature) {
- const fieldName = await field.getName();
- fields.push({ name: fieldName, widget, page: i });
- }
- }
- }
- }
- } catch (error) {
- console.warn("기존 필드 확인 중 에러:", error);
- }
-
- return fields;
- }
-
- // 텍스트 패턴 기반 서명 위치 감지
- private async detectSignatureLocations(doc: any): Promise<DetectedSignatureLocation[]> {
- const { Core } = this.instance;
- const detectedLocations: DetectedSignatureLocation[] = [];
-
- try {
- const pageCount = await doc.getPageCount();
-
- for (let pageNum = 1; pageNum <= pageCount; pageNum++) {
- const page = await doc.getPage(pageNum);
-
- // 텍스트 추출
- const textExtractor = await Core.PDFNet.TextExtractor.create();
- await textExtractor.begin(page);
-
- // 텍스트와 위치 정보 추출
- const wordList = [];
- let line = await textExtractor.getFirstLine();
-
- while (line) {
- let word = await line.getFirstWord();
- while (word) {
- const wordText = await word.getString();
- const wordBox = await word.getBBox();
-
- wordList.push({
- text: wordText,
- x1: await wordBox.getX1(),
- y1: await wordBox.getY1(),
- x2: await wordBox.getX2(),
- y2: await wordBox.getY2()
- });
-
- word = await word.getNext();
- }
- line = await line.getNext();
- }
-
- // 전체 페이지 텍스트 조합
- const fullText = wordList.map(w => w.text).join(' ');
-
- // 패턴 매칭
- for (const pattern of this.signaturePatterns) {
- const matches = Array.from(fullText.matchAll(pattern.regex));
-
- for (const match of matches) {
- // 매치된 텍스트의 위치 찾기
- const matchText = match[0];
- const matchStart = match.index || 0;
-
- // 대략적인 위치 계산 (개선 가능)
- const location = this.calculateTextLocation(wordList, matchStart, matchText.length);
-
- if (location) {
- detectedLocations.push({
- pageIndex: pageNum - 1,
- text: matchText,
- rect: {
- x1: location.x1 + (pattern.offsetX || 0),
- y1: location.y1 + (pattern.offsetY || 0),
- x2: location.x1 + (pattern.offsetX || 0) + (pattern.width || 150),
- y2: location.y1 + (pattern.offsetY || 0) + (pattern.height || 40)
- },
- pattern: pattern,
- confidence: pattern.priority
- });
-
- console.log(`🎯 패턴 매치: "${matchText}" (${pattern.name}) 페이지 ${pageNum}`);
- }
- }
- }
- }
-
- // 신뢰도 순으로 정렬 (중복 제거 포함)
- return this.deduplicateAndSort(detectedLocations);
-
- } catch (error) {
- console.error("텍스트 패턴 감지 실패:", error);
- return [];
- }
- }
-
- // 텍스트 위치 계산 (개선된 버전)
- private calculateTextLocation(wordList: any[], startIndex: number, length: number): any {
- if (wordList.length === 0) return null;
-
- // 간단한 구현: 첫 번째 단어의 위치 사용
- // 실제로는 더 정교한 텍스트 매칭 필요
- const totalChars = wordList.map(w => w.text).join(' ').length;
- const ratio = startIndex / totalChars;
- const targetWordIndex = Math.floor(ratio * wordList.length);
-
- const targetWord = wordList[Math.min(targetWordIndex, wordList.length - 1)];
- return targetWord;
- }
-
- // 중복 제거 및 정렬
- private deduplicateAndSort(locations: DetectedSignatureLocation[]): DetectedSignatureLocation[] {
- // 같은 페이지의 너무 가까운 위치들 제거
- const filtered = locations.filter((loc, index) => {
- return !locations.slice(0, index).some(prevLoc =>
- prevLoc.pageIndex === loc.pageIndex &&
- Math.abs(prevLoc.rect.x1 - loc.rect.x1) < 100 &&
- Math.abs(prevLoc.rect.y1 - loc.rect.y1) < 50
- );
- });
-
- // 신뢰도(우선순위) 순으로 정렬
- return filtered.sort((a, b) => b.confidence - a.confidence);
- }
-
- // 감지된 위치에 서명 필드 생성
- private async createSignatureFieldAtLocation(doc: any, location: DetectedSignatureLocation): Promise<string> {
- const { Core } = this.instance;
-
- const fieldName = `auto_signature_${location.pageIndex + 1}_${Date.now()}`;
- const page = await doc.getPage(location.pageIndex + 1);
-
- // 디지털 서명 필드 생성
- const sigField = await doc.createDigitalSignatureField(fieldName);
-
- // 서명 위젯 생성
- const rect = await Core.PDFNet.Rect.init(
- location.rect.x1,
- location.rect.y1,
- location.rect.x2,
- location.rect.y2
- );
-
- const widget = await Core.PDFNet.SignatureWidget.createWithDigitalSignatureField(
- doc, rect, sigField
- );
-
- // 위젯 스타일 설정
- await widget.setBackgroundColor(
- await Core.PDFNet.ColorPt.init(0.95, 0.95, 1.0), // 연한 파란색
- 3 // RGB
- );
-
- await widget.setBorderColor(
- await Core.PDFNet.ColorPt.init(0.2, 0.4, 0.8), // 파란색 테두리
- 3 // RGB
- );
-
- // 페이지에 위젯 추가
- await page.annotPushBack(widget);
-
- console.log(`✅ 자동 서명 필드 생성: ${fieldName} (패턴: ${location.pattern.name})`);
- return fieldName;
- }
-
- // 기본 서명 필드 생성 (패턴을 찾지 못한 경우)
- private async createDefaultSignatureField(doc: any): Promise<string> {
- const { Core } = this.instance;
-
- console.log("⚠️ 서명 패턴 미발견, 기본 위치에 서명 필드 생성");
-
- const pageCount = await doc.getPageCount();
- const lastPage = await doc.getPage(pageCount);
- const pageInfo = await lastPage.getPageInfo();
- const pageWidth = await pageInfo.getWidth();
- const pageHeight = await pageInfo.getHeight();
-
- const fieldName = `default_signature_${Date.now()}`;
- const sigField = await doc.createDigitalSignatureField(fieldName);
-
- // 마지막 페이지 하단 중앙에 배치
- const rect = await Core.PDFNet.Rect.init(
- pageWidth * 0.3, // 페이지 너비 30% 지점
- pageHeight * 0.1, // 페이지 하단 10% 지점
- pageWidth * 0.7, // 페이지 너비 70% 지점
- pageHeight * 0.2 // 페이지 하단 20% 지점
- );
-
- const widget = await Core.PDFNet.SignatureWidget.createWithDigitalSignatureField(
- doc, rect, sigField
- );
-
- await widget.setBackgroundColor(
- await Core.PDFNet.ColorPt.init(1.0, 0.95, 0.95), // 연한 핑크색 (주의 표시)
- 3
- );
-
- await widget.setBorderColor(
- await Core.PDFNet.ColorPt.init(0.8, 0.2, 0.2), // 빨간색 테두리
- 3
- );
-
- await lastPage.annotPushBack(widget);
-
- return fieldName;
- }
- }
-
- // ✅ BasicContractSignViewer에 통합할 수 있는 함수
- export async function addAutoSignatureFieldsToDocument(instance: WebViewerInstance): Promise<string[]> {
- if (!instance) {
- console.warn("⚠️ WebViewer 인스턴스가 없습니다.");
- return [];
- }
-
- try {
- const detector = new AutoSignatureFieldDetector(instance);
- const createdFields = await detector.detectAndCreateSignatureFields();
-
- if (createdFields.length > 0) {
- console.log(`🎉 자동 서명 필드 생성 완료: ${createdFields.join(', ')}`);
- }
-
- return createdFields;
-
- } catch (error) {
- console.error("📛 자동 서명 필드 추가 실패:", error);
- return [];
- }
- }
-
- // ✅ 문서 로드 후 자동 호출되는 Hook
- export function useAutoSignatureFields(instance: WebViewerInstance | null) {
- const [signatureFields, setSignatureFields] = React.useState<string[]>([]);
- const [isProcessing, setIsProcessing] = React.useState(false);
-
- React.useEffect(() => {
- if (!instance) return;
-
- const { documentViewer } = instance.Core;
-
- const handleDocumentLoaded = async () => {
- try {
- setIsProcessing(true);
- console.log("📄 문서 로드 완료, 자동 서명 필드 생성 시작...");
-
- // 문서 로드 후 잠시 대기 (안정성을 위해)
- await new Promise(resolve => setTimeout(resolve, 1000));
-
- const fields = await addAutoSignatureFieldsToDocument(instance);
- setSignatureFields(fields);
-
- } catch (error) {
- console.error("📛 자동 서명 필드 처리 실패:", error);
- } finally {
- setIsProcessing(false);
- }
- };
-
- documentViewer.addEventListener('documentLoaded', handleDocumentLoaded);
-
- return () => {
- documentViewer.removeEventListener('documentLoaded', handleDocumentLoaded);
- };
- }, [instance]);
-
- return {
- signatureFields,
- isProcessing,
- hasSignatureFields: signatureFields.length > 0
- };
- } \ No newline at end of file
diff --git a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx
index 49efb551..b92df089 100644
--- a/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx
+++ b/lib/basic-contract/viewer/basic-contract-sign-viewer.tsx
@@ -150,12 +150,6 @@ class AutoSignatureFieldDetector {
console.log("📄 문서 확인 완료, 기존 필드 검사...");
- // ✅ 3단계: 기존 서명 필드 확인 (안전한 방법)
- const existingFields = await this.checkExistingFieldsSafely();
- if (existingFields.length > 0) {
- console.log(`✅ 기존 서명 필드 발견: ${existingFields.length}개`);
- return existingFields;
- }
// ✅ 4단계: 단순 기본 서명 필드 생성 (텍스트 분석 스킵)
console.log("📝 기본 서명 필드 생성...");
@@ -182,34 +176,7 @@ class AutoSignatureFieldDetector {
}
}
- // ✅ 안전한 기존 필드 확인 (PDFDoc 접근 안함)
- private async checkExistingFieldsSafely(): Promise<string[]> {
- try {
- const { annotationManager } = this.instance.Core;
- const annotations = annotationManager.getAnnotationsList();
-
- const signatureFields: string[] = [];
-
- for (const annotation of annotations) {
- try {
- if (annotation.getCustomData && annotation.getCustomData('fieldName')) {
- const fieldName = annotation.getCustomData('fieldName');
- if (fieldName.includes('signature') || fieldName.includes('서명')) {
- signatureFields.push(fieldName);
- }
- }
- } catch (annotError) {
- // 개별 어노테이션 에러 무시
- continue;
- }
- }
-
- return signatureFields;
- } catch (error) {
- console.warn("기존 필드 확인 실패 (무시):", error);
- return [];
- }
- }
+
// ✅ 초간단 서명 필드 생성 (복잡한 텍스트 분석 없이)
private async createSimpleSignatureField(): Promise<string> {
@@ -229,28 +196,40 @@ class AutoSignatureFieldDetector {
// ✅ 간단한 서명 어노테이션 생성 (PDFDoc 접근 없이)
const fieldName = `simple_signature_${Date.now()}`;
+
+ const flags = new Annotations.WidgetFlags();
+ // flags.set(Annotations.WidgetFlags.REQUIRED, true);
+ // flags.set(Annotations.WidgetFlags.READ_ONLY, true);
+
+ const formField = new Core.Annotations.Forms.Field(
+ `SignatureFormField`,
+ {
+ type: "Sig",
+ flags,
+ }
+ );
// 서명 위젯 어노테이션 생성
- const signatureWidget = new Annotations.SignatureWidgetAnnotation({
- appearance: Annotations.SignatureWidgetAnnotation.DefaultAppearance.MATERIAL_OUTLINE,
+ const signatureWidget = new Annotations.SignatureWidgetAnnotation(formField,{
+ // appearance: Annotations.SignatureWidgetAnnotation.DefaultAppearance.MATERIAL_OUTLINE,
Width: 150,
Height: 50
});
// 위치 설정 (마지막 페이지 하단)
signatureWidget.setPageNumber(pageCount);
- signatureWidget.setX(pageWidth * 0.3);
- signatureWidget.setY(pageHeight * 0.15);
+ signatureWidget.setX(pageWidth * 0.7);
+ signatureWidget.setY(pageHeight * 0.85);
signatureWidget.setWidth(150);
signatureWidget.setHeight(50);
// 필드명 설정
- signatureWidget.setFieldName(fieldName);
- signatureWidget.setCustomData('fieldName', fieldName);
+ // signatureWidget.setFieldName(fieldName);
+ // signatureWidget.setCustomData('fieldName', fieldName);
- // 스타일 설정
- signatureWidget.StrokeColor = new Annotations.Color(0, 100, 200); // 파란색
- signatureWidget.StrokeThickness = 2;
+ // // 스타일 설정
+ // signatureWidget.StrokeColor = new Annotations.Color(0, 100, 200); // 파란색
+ // signatureWidget.StrokeThickness = 2;
// 어노테이션 추가
annotationManager.addAnnotation(signatureWidget);
@@ -263,47 +242,47 @@ class AutoSignatureFieldDetector {
console.error("📛 간단 서명 필드 생성 실패:", error);
// ✅ 최후의 수단: 텍스트 어노테이션으로 안내
- return await this.createTextGuidance();
+ // return await this.createTextGuidance();
}
}
// ✅ 최후의 수단: 텍스트 안내 생성
- private async createTextGuidance(): Promise<string> {
- try {
- const { Core } = this.instance;
- const { documentViewer, annotationManager, Annotations } = Core;
+ // private async createTextGuidance(): Promise<string> {
+ // try {
+ // const { Core } = this.instance;
+ // const { documentViewer, annotationManager, Annotations } = Core;
- const pageCount = documentViewer.getPageCount();
- const pageWidth = documentViewer.getPageWidth(pageCount) || 612;
- const pageHeight = documentViewer.getPageHeight(pageCount) || 792;
+ // const pageCount = documentViewer.getPageCount();
+ // const pageWidth = documentViewer.getPageWidth(pageCount) || 612;
+ // const pageHeight = documentViewer.getPageHeight(pageCount) || 792;
- // 텍스트 어노테이션으로 서명 안내
- const textAnnot = new Annotations.FreeTextAnnotation();
- textAnnot.setPageNumber(pageCount);
- textAnnot.setX(pageWidth * 0.25);
- textAnnot.setY(pageHeight * 0.1);
- textAnnot.setWidth(pageWidth * 0.5);
- textAnnot.setHeight(60);
- textAnnot.setContents("👆 여기를 클릭하여 서명해주세요");
- textAnnot.FontSize = '14pt';
- textAnnot.TextColor = new Annotations.Color(255, 0, 0); // 빨간색
- textAnnot.StrokeColor = new Annotations.Color(255, 200, 200);
- textAnnot.FillColor = new Annotations.Color(255, 240, 240);
+ // // 텍스트 어노테이션으로 서명 안내
+ // const textAnnot = new Annotations.FreeTextAnnotation();
+ // textAnnot.setPageNumber(pageCount);
+ // textAnnot.setX(pageWidth * 0.25);
+ // textAnnot.setY(pageHeight * 0.1);
+ // textAnnot.setWidth(pageWidth * 0.5);
+ // textAnnot.setHeight(60);
+ // textAnnot.setContents("👆 여기를 클릭하여 서명해주세요");
+ // textAnnot.FontSize = '14pt';
+ // textAnnot.TextColor = new Annotations.Color(255, 0, 0); // 빨간색
+ // textAnnot.StrokeColor = new Annotations.Color(255, 200, 200);
+ // textAnnot.FillColor = new Annotations.Color(255, 240, 240);
- const fieldName = `text_guidance_${Date.now()}`;
- textAnnot.setCustomData('fieldName', fieldName);
+ // const fieldName = `text_guidance_${Date.now()}`;
+ // textAnnot.setCustomData('fieldName', fieldName);
- annotationManager.addAnnotation(textAnnot);
- annotationManager.redrawAnnotation(textAnnot);
+ // annotationManager.addAnnotation(textAnnot);
+ // annotationManager.redrawAnnotation(textAnnot);
- console.log(`✅ 텍스트 안내 생성: ${fieldName}`);
- return fieldName;
+ // console.log(`✅ 텍스트 안내 생성: ${fieldName}`);
+ // return fieldName;
- } catch (error) {
- console.error("📛 텍스트 안내 생성도 실패:", error);
- return "manual_signature_required";
- }
- }
+ // } catch (error) {
+ // console.error("📛 텍스트 안내 생성도 실패:", error);
+ // return "manual_signature_required";
+ // }
+ // }
}
function useAutoSignatureFields(instance: WebViewerInstance | null) {
@@ -692,7 +671,10 @@ useEffect(() => {
{
path: "/pdftronWeb",
licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY,
- fullAPI: true
+ fullAPI: true ,
+ disabledElements: [
+
+ ]
},
viewerElement
).then((newInstance) => {
@@ -737,6 +719,19 @@ useEffect(() => {
newInstance.UI.setMinZoomLevel('25%');
newInstance.UI.setMaxZoomLevel('400%');
+
+ newInstance.UI.disableElements([
+ "toolbarGroup-Annotate",
+ "toolbarGroup-Shapes",
+ "toolbarGroup-Insert",
+ "toolbarGroup-Edit",
+ "toolbarGroup-FillAndSign",
+ "toolbarGroup-Forms",
+ "saveAsButton",
+ "downloadButton",
+
+ ])
+
documentViewer.addEventListener('documentLoadingError', (error) => {
console.error("📛 WebViewer 문서 로딩 에러:", error);
@@ -1359,7 +1354,7 @@ const SignatureFieldsStatus = () => {
);
};
-// 인라인 뷰어 렌더링
+// 인라인 뷰어 렌더링 부분 수정
if (!isOpen && !onClose) {
return (
<div className="h-full w-full flex flex-col overflow-hidden">
@@ -1368,33 +1363,33 @@ if (!isOpen && !onClose) {
<div className="border-b bg-gray-50 px-3 py-2 flex-shrink-0">
<SignatureFieldsStatus />
<TabsList className="grid w-full h-8" style={{ gridTemplateColumns: `repeat(${allFiles.length}, 1fr)` }}>
- {allFiles.map((file, index) => {
- let tabId: string;
- if (index === 0) {
- tabId = 'main';
- } else if (file.type === 'survey') {
- tabId = 'survey';
- } else {
- const fileOnlyIndex = allFiles.slice(0, index).filter(f => f.type !== 'survey').length;
- tabId = `file-${fileOnlyIndex}`;
- }
-
- return (
- <TabsTrigger key={tabId} value={tabId} className="text-xs">
- <div className="flex items-center space-x-1">
- {file.type === 'survey' ? (
- <ClipboardList 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>
- )}
- </div>
- </TabsTrigger>
- );
-})}
+ {allFiles.map((file, index) => {
+ let tabId: string;
+ if (index === 0) {
+ tabId = 'main';
+ } else if (file.type === 'survey') {
+ tabId = 'survey';
+ } else {
+ const fileOnlyIndex = allFiles.slice(0, index).filter(f => f.type !== 'survey').length;
+ tabId = `file-${fileOnlyIndex}`;
+ }
+
+ return (
+ <TabsTrigger key={tabId} value={tabId} className="text-xs">
+ <div className="flex items-center space-x-1">
+ {file.type === 'survey' ? (
+ <ClipboardList 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>
+ )}
+ </div>
+ </TabsTrigger>
+ );
+ })}
</TabsList>
</div>
@@ -1408,37 +1403,55 @@ if (!isOpen && !onClose) {
<div
className={`absolute inset-0 ${activeTab !== 'survey' ? 'block' : 'hidden'}`}
>
- <div
- ref={viewer}
- className="w-full h-full"
- style={{ position: 'relative', minHeight: '400px' }}
- >
- {fileLoading && (
- <div className="absolute inset-0 flex flex-col items-center justify-center bg-white z-10">
- <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" />
- <p className="text-sm text-muted-foreground">문서 로딩 중...</p>
- </div>
- )}
+ {/* ✅ 수정: 동일한 구조로 통일하고 스크롤 활성화 */}
+ <div className="w-full h-full overflow-auto">
+ <div
+ ref={viewer}
+ className="w-full h-full min-h-[400px]"
+ style={{
+ position: 'relative',
+ // ✅ WebViewer가 스크롤을 제어하도록 설정
+ overflow: 'visible'
+ }}
+ >
+ {fileLoading && (
+ <div className="absolute inset-0 flex flex-col items-center justify-center bg-white z-10">
+ <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" />
+ <p className="text-sm text-muted-foreground">문서 로딩 중...</p>
+ </div>
+ )}
+ </div>
</div>
</div>
</div>
</Tabs>
) : (
- <div className="h-full w-full relative">
- <div className="absolute top-2 left-2 z-10">
+ // ✅ 수정: Tabs가 없는 경우도 동일한 구조로 변경
+ <div className="h-full w-full flex flex-col">
+ <div className="flex-shrink-0 p-2">
<SignatureFieldsStatus />
</div>
- <div
- ref={viewer}
- className="absolute inset-0"
- style={{ position: 'relative', minHeight: '400px' }}
- >
- {fileLoading && (
- <div className="absolute inset-0 flex flex-col items-center justify-center bg-white z-10">
- <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" />
- <p className="text-sm text-muted-foreground">문서 로딩 중...</p>
+ <div className="flex-1 min-h-0 overflow-hidden relative">
+ <div className="absolute inset-0">
+ <div className="w-full h-full overflow-auto">
+ <div
+ ref={viewer}
+ className="w-full h-full min-h-[400px]"
+ style={{
+ position: 'relative',
+ // ✅ WebViewer가 스크롤을 제어하도록 설정
+ overflow: 'visible'
+ }}
+ >
+ {fileLoading && (
+ <div className="absolute inset-0 flex flex-col items-center justify-center bg-white z-10">
+ <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" />
+ <p className="text-sm text-muted-foreground">문서 로딩 중...</p>
+ </div>
+ )}
+ </div>
</div>
- )}
+ </div>
</div>
</div>
)}
@@ -1446,6 +1459,132 @@ if (!isOpen && !onClose) {
);
}
+// 다이얼로그 뷰어 렌더링 부분도 동일하게 수정
+return (
+ <Dialog open={showDialog} onOpenChange={handleClose}>
+ <DialogContent className="w-[90vw] max-w-6xl h-[90vh] flex flex-col p-0">
+ <DialogHeader className="px-6 py-4 border-b flex-shrink-0">
+ <DialogTitle className="flex items-center justify-between">
+ <span>기본계약서 서명</span>
+ <SignatureFieldsStatus />
+ </DialogTitle>
+ <DialogDescription>
+ 계약서를 확인하고 서명을 진행해주세요.
+ {isComplianceTemplate && (
+ <span className="block mt-1 text-amber-600">📋 준법 설문조사를 먼저 완료해주세요.</span>
+ )}
+ {isNDATemplate && additionalFiles.length > 0 && (
+ <span className="block mt-1 text-blue-600">📎 첨부서류 {additionalFiles.length}개를 각 탭에서 확인해주세요.</span>
+ )}
+ {hasSignatureFields && (
+ <span className="block mt-1 text-green-600">
+ 🎯 서명 위치가 자동으로 감지되었습니다.
+ {signatureFields.some(f => f.includes('_text')) && (
+ <span className="block text-sm text-amber-600">
+ 💡 빨간색 텍스트로 표시된 영역을 찾아 서명해주세요.
+ </span>
+ )}
+ {signatureFields.some(f => f.startsWith('default_signature_')) && !signatureFields.some(f => f.includes('_text')) && (
+ <span className="block text-sm text-amber-600">
+ 💡 마지막 페이지 하단의 핑크색 영역에서 서명해주세요.
+ </span>
+ )}
+ </span>
+ )}
+ {autoSignError && (
+ <span className="block mt-1 text-red-600">⚠️ 자동 서명 필드 생성 실패 - 수동으로 서명 위치를 클릭해주세요.</span>
+ )}
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="flex-1 min-h-0 overflow-hidden">
+ {allFiles.length > 1 ? (
+ <Tabs value={activeTab} onValueChange={handleTabChange} className="h-full flex flex-col">
+ <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) => {
+ const tabId = index === 0 ? 'main' : file.type === 'survey' ? 'survey' : `file-${index}`;
+ return (
+ <TabsTrigger key={tabId} value={tabId} className="text-xs">
+ <div className="flex items-center space-x-1">
+ {file.type === 'survey' ? <ClipboardList 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>
+ )}
+ </div>
+ </TabsTrigger>
+ );
+ })}
+ </TabsList>
+ </div>
+
+ <div className="flex-1 min-h-0 overflow-hidden relative">
+ <div className={`absolute inset-0 p-3 ${activeTab === 'survey' ? 'block' : 'hidden'}`}>
+ <SurveyComponent />
+ </div>
+
+ <div className={`absolute inset-0 ${activeTab !== 'survey' ? 'block' : 'hidden'}`}>
+ {/* ✅ 수정: 스크롤 활성화 */}
+ <div className="w-full h-full overflow-auto">
+ <div
+ ref={viewer}
+ className="w-full h-full min-h-[400px]"
+ style={{
+ position: 'relative',
+ overflow: 'visible'
+ }}
+ >
+ {fileLoading && (
+ <div className="absolute inset-0 flex flex-col items-center justify-center bg-white z-10">
+ <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" />
+ <p className="text-sm text-muted-foreground">문서 로딩 중...</p>
+ </div>
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ </Tabs>
+ ) : (
+ // ✅ 수정: 다이얼로그에서 뷰어만 있는 경우도 동일한 구조
+ <div className="h-full flex flex-col">
+ <div className="flex-1 min-h-0 overflow-hidden relative">
+ <div className="absolute inset-0">
+ <div className="w-full h-full overflow-auto">
+ <div
+ ref={viewer}
+ className="w-full h-full min-h-[400px]"
+ style={{
+ position: 'relative',
+ overflow: 'visible'
+ }}
+ >
+ {fileLoading && (
+ <div className="absolute inset-0 flex flex-col items-center justify-center bg-white z-10">
+ <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" />
+ <p className="text-sm text-muted-foreground">문서 로딩 중...</p>
+ </div>
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ )}
+ </div>
+
+ <DialogFooter className="px-6 py-4 border-t bg-white flex-shrink-0">
+ <Button variant="outline" onClick={handleClose} disabled={fileLoading}>취소</Button>
+ <Button onClick={handleSave} disabled={fileLoading || isAutoSignProcessing}>
+ <FileSignature className="h-4 w-4 mr-2" />
+ 서명 완료
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+);
+
// 다이얼로그 뷰어 렌더링
return (
<Dialog open={showDialog} onOpenChange={handleClose}>