# 전자계약 서명 솔루션 (E-Sign) - 기술 설계 문서 > **프로젝트명**: SAM E-Sign (가칭) > **작성일**: 2026-02-12 > **버전**: v1.0 Draft > **작성자**: DX 추진팀 --- ## 1. 프로젝트 개요 ### 1.1 목적 모두싸인과 유사한 **간편 전자계약 서명 솔루션**을 자체 구축한다. 두 당사자(계약 생성자 A, 상대방 B)가 온라인으로 계약서에 서명하고, 서명 완료된 문서를 법적 효력이 있는 형태로 보관하는 시스템이다. ### 1.2 핵심 가치 | 가치 | 설명 | |------|------| | **간편함** | PDF 업로드 → 서명 위치 지정 → 링크 발송, 3단계 완료 | | **보안** | 문서 해시 검증, 본인인증, 감사 추적(Audit Trail) | | **법적 효력** | 전자서명법 제2조에 부합하는 전자서명 요건 충족 | ### 1.3 범위 (v1) | 포함 | 미포함 (v2 이후) | |------|------------------| | 2인 서명 (생성자/상대방) | N명 다자간 서명 | | PDF 문서 기반 | 워드/한글 문서 직접 편집 | | 이메일 OTP 인증 | 카카오/PASS 본인인증 | | 순차 서명 (A→B 또는 B→A) | 동시 서명 | | 캔버스 직접 서명 | 공인인증서 연동 | | 감사 추적 로그 | 블록체인 기반 공증 | | 완료 문서 PDF 다운로드 | API 외부 연동 | ### 1.4 기술 스택 | 영역 | 기술 | 비고 | |------|------|------| | Backend | Laravel 11 (PHP 8.3) | SAM API 프로젝트 확장 | | Frontend | HTMX + Alpine.js + Tailwind CSS | SAM MNG 프로젝트 확장 | | Database | MySQL 8.0 | 기존 SAM DB 공유 | | PDF 렌더링 | pdf.js (프론트) | 브라우저 PDF 표시 | | 서명 캡처 | signature_pad.js | 터치/마우스 서명 | | PDF 합성 | FPDI + FPDF (백엔드) | 원본 PDF에 서명 삽입 | | 파일 저장 | Laravel Storage (local/S3) | 암호화 저장 | | 알림 | Laravel Notification (Mail) | 이메일 발송 | --- ## 2. 시스템 아키텍처 ### 2.1 전체 구조 ``` ┌─────────────────────────────────────────────────────────────────┐ │ 사용자 (브라우저) │ ├────────────────────────┬────────────────────────────────────────┤ │ 계약 생성자 (A) │ 상대방 (B) │ │ - 로그인 사용자 │ - 비로그인 (토큰 기반 접근) │ │ - 계약서 업로드 │ - 이메일 링크로 접속 │ │ - 서명 위치 지정 │ - 본인인증 후 서명 │ └────────┬───────────────┴──────────────────┬─────────────────────┘ │ │ ▼ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Nginx (sam-nginx-1) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────────────┐ ┌──────────────────────────────┐ │ │ │ MNG (sam-mng-1) │ │ API (sam-api-1) │ │ │ │ │ │ │ │ │ │ - 계약 관리 화면 │ │ - 계약 CRUD API │ │ │ │ - PDF 뷰어/서명 UI │ │ - 서명 처리 API │ │ │ │ - 대시보드 │ │ - 인증 API (OTP) │ │ │ │ │ │ - PDF 합성 서비스 │ │ │ │ HTMX + Alpine.js │ │ - 알림 서비스 (이메일) │ │ │ │ + pdf.js │ │ - 감사 로그 서비스 │ │ │ │ + signature_pad │ │ - 문서 해시 검증 서비스 │ │ │ └──────────┬───────────┘ └──────────────┬─────────────────┘ │ │ │ │ │ │ └────────────┬───────────────────┘ │ │ ▼ │ │ ┌──────────────────────────┐ │ │ │ MySQL (sam-mysql-1) │ │ │ │ │ │ │ │ - contracts │ │ │ │ - contract_signers │ │ │ │ - contract_sign_fields │ │ │ │ - contract_audit_logs │ │ │ └──────────────────────────┘ │ │ │ │ │ ┌──────────────────────────┐ │ │ │ File Storage │ │ │ │ - 원본 PDF (암호화) │ │ │ │ - 서명 이미지 │ │ │ │ - 완료 PDF │ │ │ └──────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ``` ### 2.2 서비스 레이어 구조 ``` app/Services/ESign/ ├── ContractService.php # 계약 CRUD, 상태 관리 ├── SignatureService.php # 서명 처리, 이미지 저장 ├── PdfService.php # PDF 합성, 해시 생성/검증 ├── AuthenticationService.php # OTP 생성/검증, 토큰 관리 ├── NotificationService.php # 이메일 발송 (서명 요청, 완료 알림) └── AuditLogService.php # 감사 추적 로그 기록 ``` --- ## 3. 핵심 플로우 ### 3.1 계약 생성 플로우 ``` [A: 계약 생성자] 1. 로그인 상태에서 "새 계약" 클릭 2. 계약 정보 입력 - 계약 제목 - 계약 설명 (선택) - 서명 기한 (기본 7일) 3. PDF 파일 업로드 → 서버: 파일 저장 + SHA-256 해시 생성 4. 서명 위치 지정 화면으로 이동 → pdf.js로 PDF 렌더링 → 드래그&드롭으로 서명란 배치 → A의 서명란 (파란색) → B의 서명란 (빨간색) → 날짜 필드, 텍스트 필드 추가 가능 5. 상대방(B) 정보 입력 - 이름 - 이메일 (필수) - 전화번호 (선택) 6. 서명 순서 선택 - B 먼저 → A 확인 서명 - A 먼저 → B 확인 서명 7. "서명 요청 발송" 클릭 → 서버: contract 상태를 'pending'으로 변경 → 서버: 상대방에게 이메일 발송 (서명 링크 포함) → 감사 로그: 'contract_created', 'sign_requested' ``` ### 3.2 서명 수행 플로우 (상대방 B) ``` [B: 서명 상대방] 1. 이메일에서 서명 링크 클릭 → URL: /esign/sign/{access_token} → 서버: 토큰 유효성 검증 (만료, 사용 여부) 2. 본인인증 게이트 - 이메일로 6자리 OTP 발송 - OTP 입력 (3회 제한, 5분 유효) → 서버: 인증 성공 시 세션에 verified 상태 저장 → 감사 로그: 'identity_verified' 3. 계약서 열람 → pdf.js로 PDF 렌더링 → 서명이 필요한 위치에 하이라이트 표시 → 감사 로그: 'document_viewed' 4. 서명 수행 - 서명란 클릭 → 캔버스 서명 모달 팝업 - 터치/마우스로 서명 - "서명 완료" 클릭 → 서버: 서명 이미지 저장 (PNG, base64→file) → 서버: 서명 시각, IP, User-Agent 기록 → 감사 로그: 'signed' 5. 동의 확인 - [✓] 본 계약서의 내용을 확인하였으며 서명에 동의합니다 - [✓] 전자서명의 법적 효력에 동의합니다 - "최종 제출" 클릭 → 서버: signer 상태를 'signed'로 변경 → 서버: 다음 서명자(A)에게 알림 발송 → 감사 로그: 'consent_agreed', 'submission_completed' ``` ### 3.3 최종 서명 플로우 (생성자 A) ``` [A: 계약 생성자 - 최종 서명] 1. 알림 수신 (이메일 또는 대시보드) "상대방 OOO님이 서명을 완료했습니다" 2. 본인인증 (동일 OTP 절차) 3. 계약서 열람 (B의 서명이 표시된 상태) 4. A의 서명란에 서명 5. 최종 제출 → 서버: contract 상태를 'completed'로 변경 → 서버: PDF 합성 (원본 + A서명 + B서명 + 감사정보) → 서버: 완료 PDF에 SHA-256 해시 생성 → 서버: 양쪽에 완료 알림 + PDF 다운로드 링크 발송 → 감사 로그: 'contract_completed' ``` ### 3.4 상태 전이 다이어그램 ``` ┌───────┐ │ draft │ 계약서 작성 중 (저장만, 발송 전) └───┬───┘ │ 서명 요청 발송 ▼ ┌─────────┐ │ pending │ 서명 요청됨, 첫 서명자 대기 └────┬────┘ │ 첫 번째 서명 완료 ▼ ┌────────────────────┐ │ partially_signed │ 한쪽만 서명 완료 └─────────┬──────────┘ │ 두 번째 서명 완료 ▼ ┌───────────┐ │ completed │ 양쪽 서명 완료, PDF 합성됨 └───────────┘ [만료 시] pending / partially_signed ──→ expired (서명 기한 초과) [취소 시] draft / pending ──→ cancelled (생성자가 취소) [거절 시] pending / partially_signed ──→ rejected (서명자가 거절) ``` --- ## 4. 데이터베이스 스키마 ### 4.1 ER 다이어그램 (텍스트) ``` contracts (1) ──── (N) contract_signers │ │ │ │ (1) (1) │ │ (N) (N) contract_sign_fields contract_audit_logs ``` ### 4.2 contracts (계약서) ```sql CREATE TABLE contracts ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, -- 계약 정보 contract_code VARCHAR(30) NOT NULL UNIQUE, -- 'ESIGN-2026-000001' title VARCHAR(255) NOT NULL, -- 계약 제목 description TEXT NULL, -- 계약 설명 sign_order_type ENUM('counterpart_first', 'creator_first') DEFAULT 'counterpart_first', -- 문서 파일 original_file_path VARCHAR(500) NOT NULL, -- 원본 PDF 경로 (암호화 저장) original_file_name VARCHAR(255) NOT NULL, -- 원본 파일명 original_file_hash VARCHAR(64) NOT NULL, -- SHA-256 해시 original_file_size INT UNSIGNED NOT NULL, -- 파일 크기 (bytes) signed_file_path VARCHAR(500) NULL, -- 서명 완료 PDF 경로 signed_file_hash VARCHAR(64) NULL, -- 서명 완료 PDF 해시 -- 상태 status ENUM('draft', 'pending', 'partially_signed', 'completed', 'expired', 'cancelled', 'rejected') DEFAULT 'draft', expires_at DATETIME NOT NULL, -- 서명 기한 completed_at DATETIME NULL, -- 완료 시각 -- 생성자 created_by BIGINT UNSIGNED NOT NULL, -- 생성자 user_id created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted_at TIMESTAMP NULL, -- soft delete INDEX idx_tenant_status (tenant_id, status), INDEX idx_created_by (created_by), INDEX idx_expires_at (expires_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` ### 4.3 contract_signers (서명자) ```sql CREATE TABLE contract_signers ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, contract_id BIGINT UNSIGNED NOT NULL, -- 서명자 정보 role ENUM('creator', 'counterpart') NOT NULL, sign_order TINYINT UNSIGNED NOT NULL DEFAULT 1, -- 서명 순서 (1 or 2) name VARCHAR(100) NOT NULL, email VARCHAR(255) NOT NULL, phone VARCHAR(20) NULL, -- 접근 토큰 access_token VARCHAR(128) NOT NULL UNIQUE, -- 서명 링크용 1회성 토큰 token_expires_at DATETIME NOT NULL, -- 토큰 만료 시각 -- 인증 정보 otp_code VARCHAR(10) NULL, -- OTP 코드 (해시 저장) otp_expires_at DATETIME NULL, -- OTP 만료 시각 otp_attempts TINYINT UNSIGNED DEFAULT 0, -- OTP 시도 횟수 auth_verified_at DATETIME NULL, -- 본인인증 완료 시각 auth_method VARCHAR(20) DEFAULT 'email_otp', -- 인증 방식 -- 서명 정보 signature_image_path VARCHAR(500) NULL, -- 서명 이미지 경로 signed_at DATETIME NULL, -- 서명 시각 consent_agreed_at DATETIME NULL, -- 동의 시각 -- 서명 시점 환경 정보 sign_ip_address VARCHAR(45) NULL, -- IPv4/IPv6 sign_user_agent VARCHAR(500) NULL, -- 브라우저 정보 -- 상태 status ENUM('waiting', 'notified', 'authenticated', 'signed', 'rejected') DEFAULT 'waiting', rejected_reason TEXT NULL, -- 거절 사유 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (contract_id) REFERENCES contracts(id) ON DELETE CASCADE, INDEX idx_access_token (access_token), INDEX idx_contract_role (contract_id, role) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` ### 4.4 contract_sign_fields (서명 위치/필드) ```sql CREATE TABLE contract_sign_fields ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, contract_id BIGINT UNSIGNED NOT NULL, signer_id BIGINT UNSIGNED NOT NULL, -- contract_signers.id -- 위치 정보 page_number INT UNSIGNED NOT NULL, -- PDF 페이지 번호 (1부터) position_x DECIMAL(8,2) NOT NULL, -- X 좌표 (pt 단위) position_y DECIMAL(8,2) NOT NULL, -- Y 좌표 (pt 단위) width DECIMAL(8,2) NOT NULL, -- 너비 (pt) height DECIMAL(8,2) NOT NULL, -- 높이 (pt) -- 필드 정보 field_type ENUM('signature', 'stamp', 'text', 'date', 'checkbox') NOT NULL DEFAULT 'signature', field_label VARCHAR(100) NULL, -- 필드 라벨 (예: "갑 서명") field_value TEXT NULL, -- 입력된 값 (텍스트/날짜) is_required BOOLEAN DEFAULT TRUE, sort_order INT UNSIGNED DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (contract_id) REFERENCES contracts(id) ON DELETE CASCADE, FOREIGN KEY (signer_id) REFERENCES contract_signers(id) ON DELETE CASCADE, INDEX idx_contract_page (contract_id, page_number) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` ### 4.5 contract_audit_logs (감사 추적 로그) ```sql CREATE TABLE contract_audit_logs ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, contract_id BIGINT UNSIGNED NOT NULL, signer_id BIGINT UNSIGNED NULL, -- NULL이면 시스템 이벤트 -- 이벤트 정보 action VARCHAR(50) NOT NULL, -- 가능한 값: -- 'contract_created' : 계약서 생성 -- 'document_uploaded' : PDF 업로드 -- 'fields_configured' : 서명 위치 설정 -- 'sign_requested' : 서명 요청 발송 -- 'link_accessed' : 서명 링크 접속 -- 'otp_sent' : OTP 발송 -- 'otp_verified' : OTP 인증 성공 -- 'otp_failed' : OTP 인증 실패 -- 'document_viewed' : 계약서 열람 -- 'signed' : 서명 수행 -- 'consent_agreed' : 동의 체크 -- 'submission_completed' : 최종 제출 -- 'contract_completed' : 계약 완료 (양쪽 서명) -- 'pdf_generated' : 완료 PDF 생성 -- 'document_downloaded' : 문서 다운로드 -- 'contract_cancelled' : 계약 취소 -- 'contract_rejected' : 서명 거절 -- 'contract_expired' : 계약 만료 -- 'reminder_sent' : 리마인더 발송 -- 환경 정보 ip_address VARCHAR(45) NULL, user_agent VARCHAR(500) NULL, metadata JSON NULL, -- 추가 데이터 (유연한 확장) created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (contract_id) REFERENCES contracts(id) ON DELETE CASCADE, INDEX idx_contract_action (contract_id, action), INDEX idx_created_at (created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` --- ## 5. API 명세 ### 5.1 계약 관리 API #### 계약 목록 조회 ``` GET /api/v1/esign/contracts ``` | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | page | int | N | 페이지 번호 (기본 1) | | size | int | N | 페이지 크기 (기본 20) | | status | string | N | 상태 필터 | | search | string | N | 제목 검색 | | date_from | string | N | 시작일 | | date_to | string | N | 종료일 | **Response 200:** ```json { "data": [ { "id": 1, "contract_code": "ESIGN-2026-000001", "title": "소프트웨어 개발 용역 계약서", "status": "pending", "signers": [ { "name": "김갑순", "role": "creator", "status": "waiting" }, { "name": "박을동", "role": "counterpart", "status": "notified" } ], "expires_at": "2026-02-19T23:59:59", "created_at": "2026-02-12T10:00:00" } ], "meta": { "total": 25, "page": 1, "size": 20 } } ``` #### 계약 생성 ``` POST /api/v1/esign/contracts Content-Type: multipart/form-data ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | title | string | Y | 계약 제목 | | description | string | N | 계약 설명 | | document | file | Y | PDF 파일 (max 20MB) | | expires_days | int | N | 서명 기한 일수 (기본 7) | | sign_order_type | string | N | 'counterpart_first' 또는 'creator_first' | | counterpart_name | string | Y | 상대방 이름 | | counterpart_email | string | Y | 상대방 이메일 | | counterpart_phone | string | N | 상대방 전화번호 | **Response 201:** ```json { "data": { "id": 1, "contract_code": "ESIGN-2026-000001", "status": "draft", "original_file_hash": "a1b2c3d4e5f6...", "signers": [ { "id": 1, "role": "creator", "name": "김갑순" }, { "id": 2, "role": "counterpart", "name": "박을동" } ] } } ``` #### 계약 상세 조회 ``` GET /api/v1/esign/contracts/{id} ``` #### 계약 취소 ``` POST /api/v1/esign/contracts/{id}/cancel ``` #### 계약 통계 ``` GET /api/v1/esign/contracts/stats ``` **Response 200:** ```json { "data": { "total": 50, "draft": 3, "pending": 10, "partially_signed": 5, "completed": 28, "expired": 3, "cancelled": 1 } } ``` ### 5.2 서명 필드 API #### 서명 위치 설정 ``` POST /api/v1/esign/contracts/{id}/fields Content-Type: application/json ``` ```json { "fields": [ { "signer_id": 1, "page_number": 3, "position_x": 120.5, "position_y": 650.0, "width": 150, "height": 60, "field_type": "signature", "field_label": "갑 (생성자) 서명", "is_required": true }, { "signer_id": 2, "page_number": 3, "position_x": 350.5, "position_y": 650.0, "width": 150, "height": 60, "field_type": "signature", "field_label": "을 (상대방) 서명", "is_required": true }, { "signer_id": 1, "page_number": 3, "position_x": 120.5, "position_y": 720.0, "width": 100, "height": 25, "field_type": "date", "field_label": "서명일", "is_required": true } ] } ``` #### 서명 위치 조회 ``` GET /api/v1/esign/contracts/{id}/fields ``` ### 5.3 서명 요청 API #### 서명 요청 발송 ``` POST /api/v1/esign/contracts/{id}/send ``` > 상대방에게 이메일 발송, 상태를 `pending`으로 변경 #### 리마인더 발송 ``` POST /api/v1/esign/contracts/{id}/remind ``` ### 5.4 서명 수행 API (토큰 기반, 비로그인) #### 서명 페이지 접속 ``` GET /api/v1/esign/sign/{access_token} ``` > 토큰 검증 → 계약 정보 + 서명 필드 반환 #### OTP 발송 요청 ``` POST /api/v1/esign/sign/{access_token}/otp/send ``` **Response 200:** ```json { "message": "인증코드가 이메일로 발송되었습니다", "expires_in": 300, "remaining_attempts": 3 } ``` #### OTP 인증 ``` POST /api/v1/esign/sign/{access_token}/otp/verify ``` ```json { "otp_code": "482917" } ``` **Response 200 (성공):** ```json { "verified": true, "sign_session_token": "eyJ..." } ``` **Response 401 (실패):** ```json { "verified": false, "remaining_attempts": 2, "message": "인증코드가 일치하지 않습니다" } ``` #### 서명 제출 ``` POST /api/v1/esign/sign/{access_token}/submit Authorization: Bearer {sign_session_token} Content-Type: application/json ``` ```json { "signatures": [ { "field_id": 1, "signature_image": "data:image/png;base64,iVBORw0KGgo...", "field_type": "signature" }, { "field_id": 3, "field_value": "2026-02-12", "field_type": "date" } ], "consent_electronic_signature": true, "consent_contract_content": true } ``` #### 서명 거절 ``` POST /api/v1/esign/sign/{access_token}/reject ``` ```json { "reason": "계약 조건 수정이 필요합니다" } ``` ### 5.5 문서 API #### 원본 PDF 조회 (인증 후) ``` GET /api/v1/esign/sign/{access_token}/document Authorization: Bearer {sign_session_token} ``` > Content-Type: application/pdf #### 완료 PDF 다운로드 ``` GET /api/v1/esign/contracts/{id}/download ``` > 로그인 사용자만 접근, 완료 상태인 계약만 #### 문서 무결성 검증 ``` GET /api/v1/esign/contracts/{id}/verify ``` **Response 200:** ```json { "original_hash": "a1b2c3...", "signed_hash": "d4e5f6...", "original_integrity": true, "signed_integrity": true, "verification_time": "2026-02-12T15:30:00" } ``` ### 5.6 감사 로그 API #### 감사 로그 조회 ``` GET /api/v1/esign/contracts/{id}/audit-logs ``` **Response 200:** ```json { "data": [ { "action": "contract_created", "signer_name": "김갑순", "ip_address": "192.168.1.100", "created_at": "2026-02-12T10:00:00" }, { "action": "sign_requested", "signer_name": null, "metadata": { "sent_to": "park@example.com" }, "created_at": "2026-02-12T10:05:00" } ] } ``` --- ## 6. 보안 설계 ### 6.1 문서 무결성 (Document Integrity) ```php // 업로드 시 해시 생성 $hash = hash_file('sha256', $uploadedFile->getRealPath()); // 검증 시 해시 비교 $currentHash = hash_file('sha256', Storage::path($contract->original_file_path)); $isValid = hash_equals($contract->original_file_hash, $currentHash); ``` - 원본 PDF 업로드 시 SHA-256 해시 생성 및 DB 저장 - 서명 완료 PDF 생성 시에도 별도 해시 저장 - 문서 다운로드/열람 시 해시 비교로 위변조 여부 확인 ### 6.2 서명자 인증 (Signer Authentication) ``` 인증 플로우: 1. 서명 링크 접속 (access_token 검증) 2. OTP 발송 (이메일) 3. OTP 입력 (6자리, 5분 유효, 3회 제한) 4. 인증 성공 → sign_session_token 발급 (JWT, 30분 유효) 5. 이후 모든 서명 API 호출 시 sign_session_token 필요 ``` **OTP 보안 규칙:** | 규칙 | 값 | 설명 | |------|-----|------| | OTP 길이 | 6자리 숫자 | 무작위 생성 | | 유효 시간 | 5분 | 초과 시 재발송 필요 | | 시도 제한 | 3회 | 초과 시 토큰 무효화 | | 재발송 간격 | 60초 | 연속 발송 방지 | | 저장 방식 | bcrypt 해시 | DB에 평문 저장 금지 | ### 6.3 접근 제어 (Access Control) **토큰 정책:** | 토큰 | 용도 | 유효기간 | 특성 | |------|------|---------|------| | access_token | 서명 링크 URL | 계약 만료일까지 | 128자 랜덤, URL-safe | | sign_session_token | OTP 인증 후 세션 | 30분 | JWT, 갱신 불가 | **접근 규칙:** - 서명 링크는 해당 서명자만 접근 가능 (토큰 + 이메일 검증) - 완료/취소/만료 상태 계약은 서명 접근 차단 - 서명 순서가 아닌 서명자는 대기 화면 표시 - 모든 API 호출 시 IP/UA 기록 ### 6.4 감사 추적 (Audit Trail) 모든 주요 행위를 `contract_audit_logs`에 기록: ``` 기록 대상 행위: - 계약서 생성/수정/삭제 - PDF 업로드 - 서명 요청 발송 - 서명 링크 접속 (성공/실패) - OTP 발송/검증 (성공/실패) - 계약서 열람 - 서명 수행 - 동의 체크 - 문서 다운로드 - 계약 취소/거절/만료 ``` **감사 로그는 삭제 불가** (soft delete 미적용) ### 6.5 파일 보안 ``` 저장 구조: storage/app/esign/ ├── originals/ # 원본 PDF (AES-256 암호화) │ └── {contract_id}/ │ └── {hash}.pdf.enc ├── signatures/ # 서명 이미지 │ └── {signer_id}/ │ └── {timestamp}.png └── completed/ # 완료 PDF └── {contract_id}/ └── {contract_code}_signed.pdf ``` - 원본 PDF는 AES-256으로 암호화 저장 - 서명 이미지는 별도 디렉토리에 격리 - 완료 PDF는 감사 증적 페이지를 포함하여 생성 - 파일 경로에 직접 접근 불가 (Controller를 통한 스트리밍만 허용) ### 6.6 완료 PDF에 포함되는 감사 정보 서명 완료 PDF의 마지막 페이지에 자동 추가: ``` ┌──────────────────────────────────────────┐ │ 전자서명 감사 증적 (Audit Trail) │ ├──────────────────────────────────────────┤ │ 계약 번호: ESIGN-2026-000001 │ │ 문서 해시: a1b2c3d4e5f6... │ │ │ │ [서명자 A - 갑] │ │ 이름: 김갑순 │ │ 인증 방식: 이메일 OTP │ │ 인증 시각: 2026-02-12 14:30:22 KST │ │ 서명 시각: 2026-02-12 14:32:15 KST │ │ IP: 203.xxx.xxx.100 │ │ │ │ [서명자 B - 을] │ │ 이름: 박을동 │ │ 인증 방식: 이메일 OTP │ │ 인증 시각: 2026-02-12 11:20:05 KST │ │ 서명 시각: 2026-02-12 11:23:41 KST │ │ IP: 121.xxx.xxx.55 │ │ │ │ 계약 완료: 2026-02-12 14:32:15 KST │ │ 본 문서는 전자서명법에 의거하여 │ │ 법적 효력을 가집니다. │ └──────────────────────────────────────────┘ ``` --- ## 7. 화면 목록 ### 7.1 계약 생성자(A) 화면 | # | 화면ID | 화면명 | 경로 | 설명 | |---|--------|--------|------|------| | 1 | ES_DASH | 대시보드 | /esign | 계약 현황 통계 + 목록 | | 2 | ES_CREATE | 계약 생성 | /esign/create | PDF 업로드 + 정보 입력 | | 3 | ES_FIELDS | 서명 위치 지정 | /esign/{id}/fields | PDF 위에 서명란 배치 | | 4 | ES_SEND | 서명 요청 발송 | /esign/{id}/send | 상대방 정보 입력 + 발송 | | 5 | ES_DETAIL | 계약 상세 | /esign/{id} | 진행 상태 + 감사 로그 | ### 7.2 서명 상대방(B) 화면 | # | 화면ID | 화면명 | 경로 | 설명 | |---|--------|--------|------|------| | 6 | ES_AUTH | 본인인증 | /esign/sign/{token} | OTP 인증 게이트 | | 7 | ES_SIGN | 서명 수행 | /esign/sign/{token}/sign | PDF 열람 + 서명 | | 8 | ES_DONE | 서명 완료 | /esign/sign/{token}/done | 완료 안내 | --- ## 8. 구현 로드맵 ### Phase 1: 기본 기능 (2주) | 주차 | 작업 | 담당 | |------|------|------| | 1주차 | DB 마이그레이션 생성 | API | | 1주차 | Contract 모델/서비스/컨트롤러 | API | | 1주차 | PDF 업로드 + 해시 생성 | API | | 1주차 | 대시보드 + 계약 생성 화면 | MNG | | 2주차 | 서명 위치 지정 화면 (pdf.js + 드래그) | MNG | | 2주차 | OTP 인증 + 서명 캡처 (signature_pad) | MNG + API | | 2주차 | 이메일 발송 (서명 요청/완료) | API | | 2주차 | PDF 합성 (FPDI + 서명 이미지) | API | ### Phase 2: 보안 강화 (1주) | 작업 | 담당 | |------|------| | 감사 추적 로그 전체 구현 | API | | 파일 암호화 저장 (AES-256) | API | | 완료 PDF 감사 증적 페이지 추가 | API | | 문서 무결성 검증 API | API | | 토큰 만료/사용 횟수 제한 | API | | Rate Limiting (OTP 발송 등) | API | ### Phase 3: UX 개선 (1주) | 작업 | 담당 | |------|------| | 리마인더 자동 발송 (만료 3일 전) | API (Scheduler) | | 만료 자동 처리 배치 | API (Scheduler) | | 모바일 반응형 서명 UI | MNG | | 계약 목록 필터/정렬/검색 | MNG + API | | 서명 거절 + 사유 입력 | MNG + API | ### Phase 4: 확장 기능 (v2, 추후) | 기능 | 설명 | |------|------| | SMS 인증 | Coolsms/NHN Cloud 연동 | | 카카오 알림톡 | 카카오 비즈메시지 연동 | | 다자간 서명 (3인 이상) | signers 테이블 확장 | | 템플릿 관리 | 자주 쓰는 계약서 양식 저장 | | 외부 API 제공 | 타 시스템에서 전자계약 호출 | | 블록체인 공증 | 계약 해시를 블록체인에 기록 | --- ## 9. 법적 고려사항 ### 9.1 전자서명법 요건 한국 전자서명법 제2조에 따른 전자서명 요건: | 요건 | 충족 방안 | |------|-----------| | 서명자 확인 | 이메일 OTP 본인인증 | | 서명 의사 확인 | 동의 체크박스 2개 (내용 확인 + 법적 효력 동의) | | 문서 변경 감지 | SHA-256 해시 비교 | | 서명 후 변경 불가 | 서명 완료 후 계약 수정 차단 + 별도 PDF 생성 | ### 9.2 개인정보보호 | 항목 | 조치 | |------|------| | 수집 정보 | 이름, 이메일, 전화번호 (선택), IP, 서명 이미지 | | 보관 기간 | 계약 완료 후 5년 (전자상거래법) | | 암호화 | 파일 AES-256, OTP bcrypt, 통신 HTTPS | | 접근 통제 | 계약 당사자 + 테넌트 관리자만 접근 | --- *이 문서는 SAM E-Sign 솔루션의 초기 기술 설계입니다. 구현 과정에서 상세 내용이 변경될 수 있습니다.*