docs:E-Sign 기술 설계 문서 보완 (실제 구현 반영)

- 테이블명 수정 (contracts→esign_contracts 등 4개)
- 서비스 클래스명 실제 구현과 일치 (4개→4개)
- 계약코드 형식 수정 (ESIGN-→ES-YYYYMMDD-XXXXXX)
- OTP 시도횟수 수정 (3회→5회)
- 좌표 단위 수정 (pt→% 0~100)
- 구현 파일 구조 섹션 추가 (API 19개, MNG 12개)
- 모델 상세 섹션 추가 (Traits, 상수, 관계도)
- FormRequest 검증 규칙 섹션 추가 (4개)
- 에러 처리 패턴 및 i18n 메시지 키 섹션 추가
- Multi-tenant 아키텍처 섹션 추가
- 프론트엔드 아키텍처 섹션 추가 (React 하이브리드, HTMX)
- 미구현 기능 목록 섹션 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-12 08:27:09 +09:00
parent 23170df19b
commit 8dc8fe0d0b

View File

@@ -2,7 +2,7 @@
> **프로젝트명**: SAM E-Sign (가칭)
> **작성일**: 2026-02-12
> **버전**: v1.0 Draft
> **버전**: v1.0 (구현 완료)
> **작성자**: DX 추진팀
---
@@ -86,10 +86,10 @@
│ ┌──────────────────────────┐ │
│ │ MySQL (sam-mysql-1) │ │
│ │ │ │
│ │ - contracts │ │
│ │ - contract_signers │ │
│ │ - contract_sign_fields │ │
│ │ - contract_audit_logs │ │
│ │ - esign_contracts │ │
│ │ - esign_signers │ │
│ │ - esign_sign_fields │ │
│ │ - esign_audit_logs │ │
│ └──────────────────────────┘ │
│ │ │
│ ┌──────────────────────────┐ │
@@ -105,12 +105,10 @@
```
app/Services/ESign/
├── ContractService.php # 계약 CRUD, 상태 관리
├── SignatureService.php # 서명 처리, 이미지 저장
├── PdfService.php # PDF 합성, 해시 생성/검증
── AuthenticationService.php # OTP 생성/검증, 토큰 관리
├── NotificationService.php # 이메일 발송 (서명 요청, 완료 알림)
└── AuditLogService.php # 감사 추적 로그 기록
├── EsignContractService.php # 계약 CRUD, 상태 관리, 발송/리마인더
├── EsignSignService.php # 서명 처리, OTP 인증, 토큰 관리
├── EsignPdfService.php # PDF 합성, 해시 생성/검증
── EsignAuditService.php # 감사 추적 로그 기록
```
---
@@ -158,7 +156,7 @@ app/Services/ESign/
→ 서버: 토큰 유효성 검증 (만료, 사용 여부)
2. 본인인증 게이트
- 이메일로 6자리 OTP 발송
- OTP 입력 (3회 제한, 5분 유효)
- OTP 입력 (5회 제한, 5분 유효)
→ 서버: 인증 성공 시 세션에 verified 상태 저장
→ 감사 로그: 'identity_verified'
3. 계약서 열람
@@ -238,24 +236,24 @@ app/Services/ESign/
### 4.1 ER 다이어그램 (텍스트)
```
contracts (1) ──── (N) contract_signers
(1) (1)
(N) (N)
contract_sign_fields contract_audit_logs
esign_contracts (1) ──── (N) esign_signers
(1) (1)
(N) (N)
esign_sign_fields esign_audit_logs
```
### 4.2 contracts (계약서)
### 4.2 esign_contracts (계약서)
```sql
CREATE TABLE contracts (
CREATE TABLE esign_contracts (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
-- 계약 정보
contract_code VARCHAR(30) NOT NULL UNIQUE, -- 'ESIGN-2026-000001'
contract_code VARCHAR(30) NOT NULL UNIQUE, -- 'ES-20260212-A1B2C3'
title VARCHAR(255) NOT NULL, -- 계약 제목
description TEXT NULL, -- 계약 설명
sign_order_type ENUM('counterpart_first', 'creator_first') DEFAULT 'counterpart_first',
@@ -286,10 +284,10 @@ CREATE TABLE contracts (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
### 4.3 contract_signers (서명자)
### 4.3 esign_signers (서명자)
```sql
CREATE TABLE contract_signers (
CREATE TABLE esign_signers (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
contract_id BIGINT UNSIGNED NOT NULL,
@@ -328,26 +326,26 @@ CREATE TABLE contract_signers (
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,
FOREIGN KEY (contract_id) REFERENCES esign_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 (서명 위치/필드)
### 4.4 esign_sign_fields (서명 위치/필드)
```sql
CREATE TABLE contract_sign_fields (
CREATE TABLE esign_sign_fields (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
contract_id BIGINT UNSIGNED NOT NULL,
signer_id BIGINT UNSIGNED NOT NULL, -- contract_signers.id
signer_id BIGINT UNSIGNED NOT NULL, -- esign_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)
position_x DECIMAL(8,4) NOT NULL, -- X 좌표 (% 단위, 0~100)
position_y DECIMAL(8,4) NOT NULL, -- Y 좌표 (% 단위, 0~100)
width DECIMAL(8,4) NOT NULL, -- 너비 (% 단위, 1~100)
height DECIMAL(8,4) NOT NULL, -- 높이 (% 단위, 1~100)
-- 필드 정보
field_type ENUM('signature', 'stamp', 'text', 'date', 'checkbox') NOT NULL DEFAULT 'signature',
@@ -358,16 +356,16 @@ CREATE TABLE contract_sign_fields (
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,
FOREIGN KEY (contract_id) REFERENCES esign_contracts(id) ON DELETE CASCADE,
FOREIGN KEY (signer_id) REFERENCES esign_signers(id) ON DELETE CASCADE,
INDEX idx_contract_page (contract_id, page_number)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
### 4.5 contract_audit_logs (감사 추적 로그)
### 4.5 esign_audit_logs (감사 추적 로그)
```sql
CREATE TABLE contract_audit_logs (
CREATE TABLE esign_audit_logs (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
contract_id BIGINT UNSIGNED NOT NULL,
signer_id BIGINT UNSIGNED NULL, -- NULL이면 시스템 이벤트
@@ -402,7 +400,7 @@ CREATE TABLE contract_audit_logs (
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (contract_id) REFERENCES contracts(id) ON DELETE CASCADE,
FOREIGN KEY (contract_id) REFERENCES esign_contracts(id) ON DELETE CASCADE,
INDEX idx_contract_action (contract_id, action),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@@ -434,7 +432,7 @@ GET /api/v1/esign/contracts
"data": [
{
"id": 1,
"contract_code": "ESIGN-2026-000001",
"contract_code": "ES-20260212-A1B2C3",
"title": "소프트웨어 개발 용역 계약서",
"status": "pending",
"signers": [
@@ -471,7 +469,7 @@ Content-Type: multipart/form-data
{
"data": {
"id": 1,
"contract_code": "ESIGN-2026-000001",
"contract_code": "ES-20260212-A1B2C3",
"status": "draft",
"original_file_hash": "a1b2c3d4e5f6...",
"signers": [
@@ -526,10 +524,10 @@ Content-Type: application/json
{
"signer_id": 1,
"page_number": 3,
"position_x": 120.5,
"position_y": 650.0,
"width": 150,
"height": 60,
"position_x": 15.5,
"position_y": 82.0,
"width": 20.0,
"height": 8.0,
"field_type": "signature",
"field_label": "갑 (생성자) 서명",
"is_required": true
@@ -537,10 +535,10 @@ Content-Type: application/json
{
"signer_id": 2,
"page_number": 3,
"position_x": 350.5,
"position_y": 650.0,
"width": 150,
"height": 60,
"position_x": 55.5,
"position_y": 82.0,
"width": 20.0,
"height": 8.0,
"field_type": "signature",
"field_label": "을 (상대방) 서명",
"is_required": true
@@ -548,10 +546,10 @@ Content-Type: application/json
{
"signer_id": 1,
"page_number": 3,
"position_x": 120.5,
"position_y": 720.0,
"width": 100,
"height": 25,
"position_x": 15.5,
"position_y": 92.0,
"width": 15.0,
"height": 4.0,
"field_type": "date",
"field_label": "서명일",
"is_required": true
@@ -596,7 +594,7 @@ POST /api/v1/esign/sign/{access_token}/otp/send
{
"message": "인증코드가 이메일로 발송되었습니다",
"expires_in": 300,
"remaining_attempts": 3
"remaining_attempts": 5
}
```
@@ -748,7 +746,7 @@ $isValid = hash_equals($contract->original_file_hash, $currentHash);
인증 플로우:
1. 서명 링크 접속 (access_token 검증)
2. OTP 발송 (이메일)
3. OTP 입력 (6자리, 5분 유효, 3회 제한)
3. OTP 입력 (6자리, 5분 유효, 5회 제한)
4. 인증 성공 → sign_session_token 발급 (JWT, 30분 유효)
5. 이후 모든 서명 API 호출 시 sign_session_token 필요
```
@@ -759,7 +757,7 @@ $isValid = hash_equals($contract->original_file_hash, $currentHash);
|------|-----|------|
| OTP 길이 | 6자리 숫자 | 무작위 생성 |
| 유효 시간 | 5분 | 초과 시 재발송 필요 |
| 시도 제한 | 3회 | 초과 시 토큰 무효화 |
| 시도 제한 | 5회 | 초과 시 토큰 무효화 |
| 재발송 간격 | 60초 | 연속 발송 방지 |
| 저장 방식 | bcrypt 해시 | DB에 평문 저장 금지 |
@@ -827,7 +825,7 @@ storage/app/esign/
┌──────────────────────────────────────────┐
│ 전자서명 감사 증적 (Audit Trail) │
├──────────────────────────────────────────┤
│ 계약 번호: ESIGN-2026-000001
│ 계약 번호: ES-20260212-A1B2C3
│ 문서 해시: a1b2c3d4e5f6... │
│ │
│ [서명자 A - 갑] │
@@ -947,4 +945,385 @@ storage/app/esign/
---
*이 문서는 SAM E-Sign 솔루션의 초기 기술 설계입니다. 구현 과정에서 상세 내용이 변경될 수 있습니다.*
## 10. 구현 파일 구조
### 10.1 API 프로젝트 (`/home/aweso/sam/api`)
```
database/migrations/
├── 2026_02_12_100000_create_esign_contracts_table.php
├── 2026_02_12_110000_create_esign_signers_table.php
├── 2026_02_12_120000_create_esign_sign_fields_table.php
└── 2026_02_12_130000_create_esign_audit_logs_table.php
app/Models/ESign/
├── EsignContract.php
├── EsignSigner.php
├── EsignSignField.php
└── EsignAuditLog.php
app/Services/ESign/
├── EsignContractService.php
├── EsignSignService.php
├── EsignPdfService.php
└── EsignAuditService.php
app/Http/Controllers/Api/V1/ESign/
├── EsignContractController.php # 인증 필요 (10 엔드포인트)
└── EsignSignController.php # 토큰 기반 (6 엔드포인트)
app/Http/Requests/ESign/
├── ContractStoreRequest.php
├── FieldConfigureRequest.php
├── SignSubmitRequest.php
└── SignRejectRequest.php
app/Mail/
└── EsignRequestMail.php
routes/api/v1/
└── esign.php # 16 엔드포인트 정의
```
### 10.2 MNG 프로젝트 (`/home/aweso/sam/mng`)
```
app/Http/Controllers/ESign/
├── EsignController.php # 인증 필요 (5 화면)
└── EsignPublicController.php # 비인증 (3 화면)
resources/views/esign/
├── dashboard.blade.php # 대시보드 (React)
├── create.blade.php # 계약 생성 (React)
├── detail.blade.php # 계약 상세 (React)
├── fields.blade.php # 서명 위치 지정 (React + PDF.js)
├── send.blade.php # 서명 요청 발송 (React)
└── sign/
├── auth.blade.php # 본인인증 OTP (React)
├── sign.blade.php # 서명 수행 (React + SignaturePad)
└── done.blade.php # 서명 완료 (React)
routes/web.php # esign 라우트 그룹 추가
```
---
## 11. 모델 상세
### 11.1 공통 Traits
| Trait | 적용 모델 | 기능 |
|-------|----------|------|
| `BelongsToTenant` | 전체 4개 | tenant_id 기반 글로벌 스코프, 다중 테넌트 격리 |
| `Auditable` | EsignContract | created_by, updated_by, deleted_by 자동 기록 |
| `SoftDeletes` | EsignContract | 논리 삭제 (deleted_at) |
### 11.2 EsignContract 상수
```php
// 계약 상태
const STATUS_DRAFT = 'draft';
const STATUS_PENDING = 'pending';
const STATUS_PARTIALLY_SIGNED = 'partially_signed';
const STATUS_COMPLETED = 'completed';
const STATUS_EXPIRED = 'expired';
const STATUS_CANCELLED = 'cancelled';
const STATUS_REJECTED = 'rejected';
// 서명 순서
const SIGN_ORDER_COUNTERPART_FIRST = 'counterpart_first';
const SIGN_ORDER_CREATOR_FIRST = 'creator_first';
```
### 11.3 EsignSigner 상수 및 숨김 필드
```php
// 역할
const ROLE_CREATOR = 'creator';
const ROLE_COUNTERPART = 'counterpart';
// 서명자 상태
const STATUS_WAITING = 'waiting';
const STATUS_NOTIFIED = 'notified';
const STATUS_AUTHENTICATED = 'authenticated';
const STATUS_SIGNED = 'signed';
const STATUS_REJECTED = 'rejected';
// API 응답에서 제외 (보안)
protected $hidden = ['access_token', 'otp_code'];
```
### 11.4 EsignAuditLog 액션 타입
```php
const ACTION_CREATED = 'created';
const ACTION_SENT = 'sent';
const ACTION_VIEWED = 'viewed';
const ACTION_OTP_SENT = 'otp_sent';
const ACTION_AUTHENTICATED = 'authenticated';
const ACTION_SIGNED = 'signed';
const ACTION_REJECTED = 'rejected';
const ACTION_COMPLETED = 'completed';
const ACTION_CANCELLED = 'cancelled';
const ACTION_REMINDED = 'reminded';
const ACTION_DOWNLOADED = 'downloaded';
```
### 11.5 모델 관계도
```
EsignContract
├── signers() → HasMany → EsignSigner
├── signFields() → HasMany → EsignSignField
├── auditLogs() → HasMany → EsignAuditLog
└── creator() → BelongsTo → User
EsignSigner
├── contract() → BelongsTo → EsignContract
└── signFields() → HasMany → EsignSignField
EsignSignField
├── contract() → BelongsTo → EsignContract
└── signer() → BelongsTo → EsignSigner
EsignAuditLog
├── contract() → BelongsTo → EsignContract
└── signer() → BelongsTo → EsignSigner
```
---
## 12. FormRequest 검증 규칙
### 12.1 ContractStoreRequest (계약 생성)
| 필드 | 규칙 | 설명 |
|------|------|------|
| title | required, string, max:200 | 계약 제목 |
| description | nullable, string, max:2000 | 계약 설명 |
| file | required, file, mimes:pdf, max:20480 | PDF 파일 (최대 20MB) |
| sign_order_type | nullable, in:counterpart_first,creator_first | 서명 순서 |
| expires_at | nullable, date, after:now | 서명 기한 |
| creator_name | required, string, max:100 | 생성자(갑) 이름 |
| creator_email | required, email, max:255 | 생성자(갑) 이메일 |
| creator_phone | nullable, string, max:20 | 생성자(갑) 전화번호 |
| counterpart_name | required, string, max:100 | 상대방(을) 이름 |
| counterpart_email | required, email, max:255 | 상대방(을) 이메일 |
| counterpart_phone | nullable, string, max:20 | 상대방(을) 전화번호 |
### 12.2 FieldConfigureRequest (서명 위치 설정)
| 필드 | 규칙 | 설명 |
|------|------|------|
| fields | required, array, min:1 | 서명 필드 배열 |
| fields.*.signer_id | required, exists:esign_signers,id | 서명자 ID |
| fields.*.page_number | required, integer, min:1 | PDF 페이지 번호 |
| fields.*.position_x | required, numeric, between:0,100 | X 좌표 (%) |
| fields.*.position_y | required, numeric, between:0,100 | Y 좌표 (%) |
| fields.*.width | required, numeric, between:1,100 | 너비 (%) |
| fields.*.height | required, numeric, between:1,100 | 높이 (%) |
| fields.*.field_type | required, in:signature,stamp,text,date,checkbox | 필드 유형 |
| fields.*.field_label | nullable, string, max:100 | 필드 라벨 |
| fields.*.is_required | nullable, boolean | 필수 여부 |
| fields.*.sort_order | nullable, integer, min:0 | 정렬 순서 |
### 12.3 SignSubmitRequest (서명 제출)
| 필드 | 규칙 | 설명 |
|------|------|------|
| signature_image | required, string | Base64 인코딩 서명 이미지 |
### 12.4 SignRejectRequest (서명 거절)
| 필드 | 규칙 | 설명 |
|------|------|------|
| reason | required, string, max:1000 | 거절 사유 |
---
## 13. 에러 처리 패턴
### 13.1 ApiResponse::handle() 패턴
모든 컨트롤러는 `ApiResponse::handle()` 래퍼를 사용하여 일관된 응답 형식을 보장합니다.
```php
// 성공 응답
return ApiResponse::handle(
fn() => $this->contractService->create($request->validated()),
__('message.esign.contract_created'), // i18n 메시지 키
201
);
// 에러 시 자동 처리
// → 400: 잘못된 요청 (Validation)
// → 403: 권한 없음
// → 404: 리소스 없음
// → 500: 서버 에러
```
### 13.2 응답 구조
```json
// 성공
{
"success": true,
"message": "계약이 성공적으로 생성되었습니다.",
"data": { ... }
}
// 실패
{
"success": false,
"message": "에러 메시지",
"errors": { ... }
}
```
### 13.3 i18n 메시지 키
```php
// 성공 메시지 (message.esign.*)
'contract_created' => '계약이 성공적으로 생성되었습니다.',
'contract_cancelled' => '계약이 취소되었습니다.',
'contract_sent' => '서명 요청이 발송되었습니다.',
'fields_configured' => '서명 위치가 설정되었습니다.',
'otp_sent' => '인증코드가 발송되었습니다.',
'otp_verified' => '본인인증이 완료되었습니다.',
'signature_submitted' => '서명이 완료되었습니다.',
'contract_rejected' => '서명이 거절되었습니다.',
// 에러 메시지 (error.esign.*)
'contract_not_found' => '계약을 찾을 수 없습니다.',
'invalid_status' => '현재 상태에서는 이 작업을 수행할 수 없습니다.',
'token_expired' => '서명 링크가 만료되었습니다.',
'otp_max_attempts' => 'OTP 입력 횟수를 초과했습니다.',
'otp_invalid' => '인증코드가 일치하지 않습니다.',
'already_signed' => '이미 서명이 완료되었습니다.',
```
---
## 14. Multi-tenant 아키텍처
### 14.1 데이터 격리
모든 E-Sign 테이블에 `tenant_id` 컬럼이 포함되어 있으며, `BelongsToTenant` trait의 글로벌 스코프에 의해 자동으로 현재 테넌트의 데이터만 조회됩니다.
```php
// BelongsToTenant trait의 글로벌 스코프
// → SELECT * FROM esign_contracts WHERE tenant_id = {현재 테넌트}
```
### 14.2 비인증 접근 시 (공개 서명)
서명자(B)는 로그인 없이 토큰 기반으로 접근하므로, 테넌트 스코프를 우회해야 합니다.
```php
// EsignAuditService::logPublic()
// → withoutGlobalScopes()를 사용하여 tenant 스코프 우회
// → tenant_id를 명시적으로 전달
public function logPublic(int $tenantId, int $contractId, string $action, ...): EsignAuditLog
{
return EsignAuditLog::withoutGlobalScopes()->create([
'tenant_id' => $tenantId,
'contract_id' => $contractId,
'action' => $action,
// ...
]);
}
```
### 14.3 계약 코드 생성
테넌트 간 충돌을 방지하기 위해 날짜 + 랜덤 문자열 형식을 사용합니다.
```
형식: ES-{YYYYMMDD}-{6자리 랜덤}
예시: ES-20260212-A1B2C3
```
---
## 15. 프론트엔드 아키텍처
### 15.1 React 하이브리드 패턴
모든 MNG 뷰는 Blade 레이아웃 안에서 React 18 컴포넌트를 마운트하는 하이브리드 방식입니다.
```
┌─────────────────────────────────────────────┐
│ Blade 레이아웃 (app.blade.php) │
│ ├── 사이드바 메뉴 (HTMX 기반) │
│ ├── 상단 헤더 │
│ └── 콘텐츠 영역 │
│ └── <div id="esign-xxx-root"></div> │
│ └── React 컴포넌트 마운트 │
│ └── API 호출 (fetch → api.sam.kr)│
└─────────────────────────────────────────────┘
```
### 15.2 CDN 의존성
```html
<!-- React 18 -->
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<!-- Babel (JSX 브라우저 트랜스파일링) -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<!-- PDF.js (서명 위치 지정, 서명 수행 화면) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<!-- SignaturePad (서명 캡처) -->
<script src="https://cdn.jsdelivr.net/npm/signature_pad@4.1.7/dist/signature_pad.umd.min.js"></script>
```
### 15.3 HTMX 네비게이션
E-Sign 화면은 React를 사용하므로 HTMX 부분 로드 시 스크립트가 실행되지 않습니다.
따라서 모든 컨트롤러에서 `HX-Redirect` 헤더를 반환하여 전체 페이지를 로드합니다.
```php
public function dashboard(Request $request): View|Response
{
if ($request->header('HX-Request')) {
return response('', 200)
->header('HX-Redirect', route('esign.dashboard'));
}
return view('esign.dashboard');
}
```
### 15.4 React Root ID 매핑
| 화면 | Root Element ID |
|------|----------------|
| 대시보드 | `#esign-dashboard-root` |
| 계약 생성 | `#esign-create-root` |
| 계약 상세 | `#esign-detail-root` |
| 서명 위치 지정 | `#esign-fields-root` |
| 서명 요청 발송 | `#esign-send-root` |
| 본인인증 OTP | `#esign-auth-root` |
| 서명 수행 | `#esign-sign-root` |
| 서명 완료 | `#esign-done-root` |
---
## 16. 미구현 기능 (v1.1 이후)
| 기능 | 현재 상태 | 구현 방안 |
|------|----------|----------|
| PDF 서명 합성 | 스텁 구현 | FPDI + FPDF 라이브러리로 원본 PDF에 서명 이미지 삽입 |
| 감사 증적 페이지 | 스텁 구현 | 완료 PDF 마지막 페이지에 서명 이력 자동 추가 |
| 파일 암호화 | 미구현 | AES-256-CBC로 원본 PDF 암호화 저장 |
| 자동 만료 처리 | 미구현 | Laravel Scheduler로 만료된 계약 상태 자동 변경 |
| 자동 리마인더 | 미구현 | 만료 3일 전 자동 알림 이메일 발송 |
| SMS OTP | 미구현 | CoolSMS/NHN Cloud 연동 |
| OTP bcrypt 해싱 | 미구현 | OTP 코드 DB 저장 시 bcrypt 적용 |
---
*이 문서는 SAM E-Sign v1.0 구현 기준 기술 설계서입니다. 최종 업데이트: 2026-02-12*