docs:E-Sign 전자계약 구현 가이드 추가
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
755
projects/e-sign/implementation-guide.md
Normal file
755
projects/e-sign/implementation-guide.md
Normal file
@@ -0,0 +1,755 @@
|
||||
# SAM E-Sign 구현 가이드
|
||||
|
||||
> **프로젝트명**: SAM E-Sign (전자계약 서명 솔루션)
|
||||
> **구현일**: 2026-02-12
|
||||
> **버전**: v1.0
|
||||
> **설계 문서**: [technical-design.md](./technical-design.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 구현 개요
|
||||
|
||||
모두싸인과 유사한 간편 전자계약 서명 솔루션을 SAM 시스템에 구현했다.
|
||||
API 프로젝트에 백엔드 로직(모델, 서비스, 컨트롤러, 라우트)을, MNG 프로젝트에 프론트엔드(컨트롤러, React 뷰)를 구축했다.
|
||||
|
||||
### 구현 범위
|
||||
|
||||
| 영역 | 내용 |
|
||||
|------|------|
|
||||
| 데이터베이스 | 마이그레이션 4개 (esign_ 접두사 테이블) |
|
||||
| 모델 | 4개 (EsignContract, EsignSigner, EsignSignField, EsignAuditLog) |
|
||||
| 서비스 | 4개 (Contract, Sign, Pdf, Audit) |
|
||||
| API 컨트롤러 | 2개 (Contract 10엔드포인트, Sign 6엔드포인트) |
|
||||
| FormRequest | 4개 (Store, FieldConfigure, SignSubmit, SignReject) |
|
||||
| 메일 | 1개 (EsignRequestMail) |
|
||||
| MNG 컨트롤러 | 2개 (인증 5화면, 공개 3화면) |
|
||||
| MNG 뷰 | 8개 (React 하이브리드) |
|
||||
| API 라우트 | 16개 |
|
||||
| MNG 라우트 | 8개 |
|
||||
| i18n | message 12키 + error 16키 |
|
||||
|
||||
**총 파일**: 신규 29개 + 수정 4개 = 33개
|
||||
|
||||
---
|
||||
|
||||
## 2. 파일 구조
|
||||
|
||||
### 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
|
||||
└── EsignSignController.php
|
||||
|
||||
app/Http/Requests/ESign/
|
||||
├── ContractStoreRequest.php
|
||||
├── FieldConfigureRequest.php
|
||||
├── SignSubmitRequest.php
|
||||
└── SignRejectRequest.php
|
||||
|
||||
app/Mail/
|
||||
└── EsignRequestMail.php
|
||||
|
||||
resources/views/emails/esign/
|
||||
└── request.blade.php
|
||||
|
||||
routes/api/v1/
|
||||
└── esign.php
|
||||
|
||||
routes/api.php # 수정 (esign.php include 추가)
|
||||
lang/ko/message.php # 수정 (esign 키 추가)
|
||||
lang/ko/error.php # 수정 (esign 키 추가)
|
||||
```
|
||||
|
||||
### MNG 프로젝트 (`/home/aweso/sam/mng`)
|
||||
|
||||
```
|
||||
app/Http/Controllers/ESign/
|
||||
├── EsignController.php # 인증 필요 (5개 메서드)
|
||||
└── EsignPublicController.php # 비인증 (3개 메서드)
|
||||
|
||||
resources/views/esign/
|
||||
├── dashboard.blade.php # 대시보드 (통계 + 목록)
|
||||
├── create.blade.php # 계약 생성 (PDF 업로드)
|
||||
├── detail.blade.php # 계약 상세 (현황 + 로그)
|
||||
├── fields.blade.php # 서명 위치 지정 (PDF.js)
|
||||
├── send.blade.php # 서명 요청 발송 확인
|
||||
└── sign/
|
||||
├── auth.blade.php # 공개 - OTP 본인인증
|
||||
├── sign.blade.php # 공개 - 서명 수행
|
||||
└── done.blade.php # 공개 - 서명 완료
|
||||
|
||||
routes/web.php # 수정 (esign 라우트 추가)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 데이터베이스 스키마
|
||||
|
||||
### 3.1 esign_contracts (계약 마스터)
|
||||
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|------|------|------|
|
||||
| id | BIGINT PK | 자동 증가 |
|
||||
| tenant_id | BIGINT FK | 테넌트 |
|
||||
| contract_code | VARCHAR(50) UNIQUE | 계약 코드 (ES-YYYYMMDD-RANDOM) |
|
||||
| title | VARCHAR(200) | 계약 제목 |
|
||||
| description | TEXT NULL | 계약 설명 |
|
||||
| sign_order_type | ENUM | counterpart_first, creator_first |
|
||||
| original_file_path | VARCHAR(500) | 원본 PDF 경로 |
|
||||
| original_file_name | VARCHAR(255) | 원본 파일명 |
|
||||
| original_file_hash | VARCHAR(64) | SHA-256 해시 |
|
||||
| original_file_size | INT UNSIGNED | 파일 크기 (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 |
|
||||
| expires_at | TIMESTAMP | 서명 기한 |
|
||||
| completed_at | TIMESTAMP NULL | 완료 시각 |
|
||||
| created_by/updated_by/deleted_by | BIGINT NULL | 감사 필드 |
|
||||
| timestamps, softDeletes | | |
|
||||
|
||||
**인덱스**: tenant_id+status, contract_code(unique), expires_at
|
||||
|
||||
### 3.2 esign_signers (서명자)
|
||||
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|------|------|------|
|
||||
| id | BIGINT PK | |
|
||||
| tenant_id | BIGINT FK | 테넌트 |
|
||||
| contract_id | BIGINT FK | → esign_contracts |
|
||||
| role | ENUM | creator, counterpart |
|
||||
| sign_order | TINYINT | 서명 순서 (1 또는 2) |
|
||||
| name | VARCHAR(100) | 서명자 이름 |
|
||||
| email | VARCHAR(255) | 이메일 |
|
||||
| phone | VARCHAR(20) NULL | 전화번호 |
|
||||
| access_token | VARCHAR(128) UNIQUE | 서명 링크 토큰 |
|
||||
| token_expires_at | TIMESTAMP | 토큰 만료 |
|
||||
| otp_code | VARCHAR(10) NULL | OTP 코드 |
|
||||
| otp_expires_at | TIMESTAMP NULL | OTP 만료 |
|
||||
| otp_attempts | TINYINT DEFAULT 0 | OTP 시도 횟수 |
|
||||
| auth_verified_at | TIMESTAMP NULL | 본인인증 완료 |
|
||||
| signature_image_path | VARCHAR(500) NULL | 서명 이미지 경로 |
|
||||
| signed_at | TIMESTAMP NULL | 서명 시각 |
|
||||
| consent_agreed_at | TIMESTAMP NULL | 동의 시각 |
|
||||
| sign_ip_address | VARCHAR(45) NULL | 서명 IP |
|
||||
| sign_user_agent | TEXT NULL | User-Agent |
|
||||
| status | ENUM | waiting/notified/authenticated/signed/rejected |
|
||||
| rejected_reason | TEXT NULL | 거절 사유 |
|
||||
|
||||
### 3.3 esign_sign_fields (서명 필드)
|
||||
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|------|------|------|
|
||||
| id | BIGINT PK | |
|
||||
| tenant_id | BIGINT FK | 테넌트 |
|
||||
| contract_id | BIGINT FK | → esign_contracts |
|
||||
| signer_id | BIGINT FK | → esign_signers |
|
||||
| page_number | INT UNSIGNED | PDF 페이지 번호 |
|
||||
| position_x | DECIMAL(8,4) | X 좌표 (%) |
|
||||
| position_y | DECIMAL(8,4) | Y 좌표 (%) |
|
||||
| width | DECIMAL(8,4) | 너비 (%) |
|
||||
| height | DECIMAL(8,4) | 높이 (%) |
|
||||
| field_type | ENUM | signature/stamp/text/date/checkbox |
|
||||
| field_label | VARCHAR(100) NULL | 필드 라벨 |
|
||||
| field_value | TEXT NULL | 입력된 값 |
|
||||
| is_required | BOOLEAN DEFAULT true | 필수 여부 |
|
||||
| sort_order | INT DEFAULT 0 | 정렬 순서 |
|
||||
|
||||
### 3.4 esign_audit_logs (감사 로그)
|
||||
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|------|------|------|
|
||||
| id | BIGINT PK | |
|
||||
| tenant_id | BIGINT FK | 테넌트 |
|
||||
| contract_id | BIGINT FK | → esign_contracts |
|
||||
| signer_id | BIGINT FK NULL | → esign_signers (NULL = 시스템 이벤트) |
|
||||
| action | VARCHAR(50) | 액션 코드 |
|
||||
| ip_address | VARCHAR(45) NULL | IP 주소 |
|
||||
| user_agent | TEXT NULL | User-Agent |
|
||||
| metadata | JSON NULL | 추가 데이터 |
|
||||
| created_at | TIMESTAMP | 생성 시각 (삭제 불가) |
|
||||
|
||||
**감사 로그 액션 코드**:
|
||||
|
||||
| 코드 | 설명 |
|
||||
|------|------|
|
||||
| created | 계약 생성 |
|
||||
| sent | 서명 요청 발송 |
|
||||
| viewed | 문서 열람 |
|
||||
| otp_sent | OTP 발송 |
|
||||
| authenticated | 본인인증 완료 |
|
||||
| signed | 서명 완료 |
|
||||
| rejected | 서명 거절 |
|
||||
| completed | 계약 완료 |
|
||||
| cancelled | 계약 취소 |
|
||||
| reminded | 리마인더 발송 |
|
||||
| downloaded | 문서 다운로드 |
|
||||
|
||||
### ER 다이어그램
|
||||
|
||||
```
|
||||
esign_contracts (1) ──── (N) esign_signers
|
||||
│ │
|
||||
(1) (1)
|
||||
│ │
|
||||
(N) (N)
|
||||
esign_sign_fields esign_audit_logs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 모델 계층
|
||||
|
||||
### 4.1 EsignContract
|
||||
|
||||
**파일**: `app/Models/ESign/EsignContract.php`
|
||||
|
||||
```php
|
||||
use Auditable, BelongsToTenant, SoftDeletes;
|
||||
|
||||
// 상수
|
||||
STATUS_DRAFT, STATUS_PENDING, STATUS_PARTIALLY_SIGNED,
|
||||
STATUS_COMPLETED, STATUS_EXPIRED, STATUS_CANCELLED, STATUS_REJECTED
|
||||
SIGN_ORDER_COUNTERPART_FIRST, SIGN_ORDER_CREATOR_FIRST
|
||||
|
||||
// 관계
|
||||
signers() → HasMany(EsignSigner)
|
||||
signFields() → HasMany(EsignSignField)
|
||||
auditLogs() → HasMany(EsignAuditLog)
|
||||
creator() → BelongsTo(User, 'created_by')
|
||||
|
||||
// 스코프
|
||||
scopeStatus($query, $status)
|
||||
scopeActive($query) // pending, partially_signed
|
||||
|
||||
// 헬퍼
|
||||
isExpired(): bool // expires_at < now
|
||||
canSign(): bool // pending 또는 partially_signed + 미만료
|
||||
getNextSigner(): ?EsignSigner // sign_order 기준 다음 서명자
|
||||
```
|
||||
|
||||
### 4.2 EsignSigner
|
||||
|
||||
**파일**: `app/Models/ESign/EsignSigner.php`
|
||||
|
||||
```php
|
||||
use BelongsToTenant;
|
||||
|
||||
// 상수
|
||||
ROLE_CREATOR, ROLE_COUNTERPART
|
||||
STATUS_WAITING, STATUS_NOTIFIED, STATUS_AUTHENTICATED, STATUS_SIGNED, STATUS_REJECTED
|
||||
|
||||
// Hidden 필드: access_token, otp_code
|
||||
|
||||
// 관계
|
||||
contract() → BelongsTo(EsignContract)
|
||||
signFields() → HasMany(EsignSignField)
|
||||
|
||||
// 헬퍼
|
||||
isVerified(): bool // auth_verified_at !== null
|
||||
hasSigned(): bool // signed_at !== null
|
||||
canSign(): bool // !hasSigned + !rejected + contract.canSign
|
||||
```
|
||||
|
||||
### 4.3 EsignSignField
|
||||
|
||||
**파일**: `app/Models/ESign/EsignSignField.php`
|
||||
|
||||
```php
|
||||
use BelongsToTenant;
|
||||
|
||||
// 상수
|
||||
TYPE_SIGNATURE, TYPE_STAMP, TYPE_TEXT, TYPE_DATE, TYPE_CHECKBOX
|
||||
|
||||
// 관계
|
||||
contract() → BelongsTo(EsignContract)
|
||||
signer() → BelongsTo(EsignSigner)
|
||||
```
|
||||
|
||||
### 4.4 EsignAuditLog
|
||||
|
||||
**파일**: `app/Models/ESign/EsignAuditLog.php`
|
||||
|
||||
```php
|
||||
use BelongsToTenant;
|
||||
|
||||
$timestamps = false; // created_at만 사용
|
||||
$casts = ['metadata' => 'array'];
|
||||
|
||||
// 상수
|
||||
ACTION_CREATED, ACTION_SENT, ACTION_VIEWED, ACTION_OTP_SENT,
|
||||
ACTION_AUTHENTICATED, ACTION_SIGNED, ACTION_REJECTED,
|
||||
ACTION_COMPLETED, ACTION_CANCELLED, ACTION_REMINDED, ACTION_DOWNLOADED
|
||||
|
||||
// 관계
|
||||
contract() → BelongsTo(EsignContract)
|
||||
signer() → BelongsTo(EsignSigner)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 서비스 계층
|
||||
|
||||
모든 서비스는 `App\Services\Service`를 상속하며, `tenantId()`와 `apiUserId()`를 사용한다.
|
||||
|
||||
### 5.1 EsignContractService
|
||||
|
||||
**파일**: `app/Services/ESign/EsignContractService.php`
|
||||
**의존성**: EsignAuditService, EsignPdfService
|
||||
|
||||
| 메서드 | 설명 |
|
||||
|--------|------|
|
||||
| `list(array $params)` | 필터/페이지네이션 목록 (status, search, date_from/to, per_page) |
|
||||
| `stats()` | 상태별 통계 (GROUP BY status) |
|
||||
| `create(array $data)` | 계약 생성 + PDF 저장 + 해시 + 서명자 2인 생성 |
|
||||
| `show(int $id)` | 상세 조회 (signers, signFields, auditLogs eager loading) |
|
||||
| `cancel(int $id)` | 계약 취소 (draft/pending만 가능) |
|
||||
| `send(int $id)` | 서명 요청 발송 (상태 → pending, 이메일 발송) |
|
||||
| `remind(int $id)` | 리마인더 발송 (다음 서명자에게 이메일 재발송) |
|
||||
| `configureFields(int $id, array $fields)` | 서명 위치 설정 (기존 삭제 후 재생성, draft만) |
|
||||
|
||||
**계약 코드 생성 규칙**: `ES-YYYYMMDD-RANDOM6` (예: ES-20260212-A3F2K9)
|
||||
|
||||
### 5.2 EsignSignService
|
||||
|
||||
**파일**: `app/Services/ESign/EsignSignService.php`
|
||||
**의존성**: EsignAuditService, EsignPdfService
|
||||
**특이사항**: 모든 쿼리에서 `withoutGlobalScopes()` 사용 (토큰 기반 공개 접근)
|
||||
|
||||
| 메서드 | 설명 |
|
||||
|--------|------|
|
||||
| `getByToken(string $token)` | 토큰으로 계약+서명자 조회 (만료/상태 검증) |
|
||||
| `sendOtp(string $token)` | 6자리 OTP 생성 + 5분 만료 + 이메일 발송 |
|
||||
| `verifyOtp(string $token, string $otpCode)` | OTP 검증 (최대 5회) → sign_session_token JWT 발급 |
|
||||
| `submitSignature(string $token, array $data)` | 서명 이미지 저장 + 자동 완료 체크 |
|
||||
| `reject(string $token, string $reason)` | 서명 거절 → 계약 상태 rejected |
|
||||
| `checkAndComplete(EsignContract)` | (private) 양쪽 서명 완료 시 자동 completed 처리 |
|
||||
|
||||
**OTP 보안 규칙**:
|
||||
- 6자리 숫자
|
||||
- 5분 유효 (`otp_expires_at`)
|
||||
- 최대 5회 시도 (`otp_attempts`)
|
||||
- 초과 시 토큰 무효화
|
||||
|
||||
### 5.3 EsignPdfService
|
||||
|
||||
**파일**: `app/Services/ESign/EsignPdfService.php`
|
||||
|
||||
| 메서드 | 설명 | 상태 |
|
||||
|--------|------|------|
|
||||
| `generateHash(string $filePath)` | SHA-256 해시 생성 | 구현 완료 |
|
||||
| `verifyIntegrity(string $filePath, string $expectedHash)` | 해시 비교 검증 (hash_equals) | 구현 완료 |
|
||||
| `composeSigned(...)` | 원본 PDF + 서명 이미지 합성 | 스텁 (FPDI 추후) |
|
||||
| `addAuditPage(...)` | 감사 증적 페이지 추가 | 스텁 (FPDI 추후) |
|
||||
|
||||
### 5.4 EsignAuditService
|
||||
|
||||
**파일**: `app/Services/ESign/EsignAuditService.php`
|
||||
|
||||
| 메서드 | 설명 |
|
||||
|--------|------|
|
||||
| `log(int $contractId, string $action, ...)` | 감사 로그 기록 (인증 컨텍스트) |
|
||||
| `logPublic(int $tenantId, int $contractId, ...)` | 감사 로그 기록 (공개 접근, withoutGlobalScopes) |
|
||||
| `getContractLogs(int $contractId)` | 계약별 감사 로그 조회 (signer 포함) |
|
||||
|
||||
---
|
||||
|
||||
## 6. API 엔드포인트
|
||||
|
||||
### 6.1 계약 관리 API (인증 필요)
|
||||
|
||||
미들웨어: `auth.apikey`
|
||||
|
||||
| Method | Path | Controller | 설명 |
|
||||
|--------|------|-----------|------|
|
||||
| GET | `/api/v1/esign/contracts` | index | 계약 목록 (필터/페이지네이션) |
|
||||
| POST | `/api/v1/esign/contracts` | store | 계약 생성 (multipart/form-data) |
|
||||
| GET | `/api/v1/esign/contracts/stats` | stats | 상태별 통계 |
|
||||
| GET | `/api/v1/esign/contracts/{id}` | show | 계약 상세 |
|
||||
| POST | `/api/v1/esign/contracts/{id}/cancel` | cancel | 계약 취소 |
|
||||
| POST | `/api/v1/esign/contracts/{id}/fields` | configureFields | 서명 위치 설정 |
|
||||
| POST | `/api/v1/esign/contracts/{id}/send` | send | 서명 요청 발송 |
|
||||
| POST | `/api/v1/esign/contracts/{id}/remind` | remind | 리마인더 발송 |
|
||||
| GET | `/api/v1/esign/contracts/{id}/download` | download | PDF 다운로드 |
|
||||
| GET | `/api/v1/esign/contracts/{id}/verify` | verify | 무결성 검증 |
|
||||
|
||||
### 6.2 서명 수행 API (토큰 기반, 비인증)
|
||||
|
||||
미들웨어: API 키만 필요 (테넌트 인증 없음)
|
||||
|
||||
| Method | Path | Controller | 설명 |
|
||||
|--------|------|-----------|------|
|
||||
| GET | `/api/v1/esign/sign/{token}` | getContract | 계약 정보 조회 |
|
||||
| POST | `/api/v1/esign/sign/{token}/otp/send` | sendOtp | OTP 발송 |
|
||||
| POST | `/api/v1/esign/sign/{token}/otp/verify` | verifyOtp | OTP 검증 |
|
||||
| GET | `/api/v1/esign/sign/{token}/document` | getDocument | PDF 스트리밍 |
|
||||
| POST | `/api/v1/esign/sign/{token}/submit` | submit | 서명 제출 |
|
||||
| POST | `/api/v1/esign/sign/{token}/reject` | reject | 서명 거절 |
|
||||
|
||||
### 6.3 응답 패턴
|
||||
|
||||
모든 API는 `ApiResponse::handle()` 패턴을 따른다:
|
||||
|
||||
```json
|
||||
// 성공
|
||||
{
|
||||
"success": true,
|
||||
"message": "처리 완료",
|
||||
"data": { ... }
|
||||
}
|
||||
|
||||
// 실패
|
||||
{
|
||||
"success": false,
|
||||
"message": "오류 메시지",
|
||||
"error": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. MNG 화면 구성
|
||||
|
||||
### 7.1 라우트 구조
|
||||
|
||||
**인증 필요** (미들웨어: auth):
|
||||
|
||||
| 경로 | 컨트롤러 | 뷰 |
|
||||
|------|---------|-----|
|
||||
| GET `/esign` | EsignController@dashboard | esign/dashboard |
|
||||
| GET `/esign/create` | EsignController@create | esign/create |
|
||||
| GET `/esign/{id}` | EsignController@detail | esign/detail |
|
||||
| GET `/esign/{id}/fields` | EsignController@fields | esign/fields |
|
||||
| GET `/esign/{id}/send` | EsignController@send | esign/send |
|
||||
|
||||
**공개** (비인증):
|
||||
|
||||
| 경로 | 컨트롤러 | 뷰 |
|
||||
|------|---------|-----|
|
||||
| GET `/esign/sign/{token}` | EsignPublicController@auth | esign/sign/auth |
|
||||
| GET `/esign/sign/{token}/sign` | EsignPublicController@sign | esign/sign/sign |
|
||||
| GET `/esign/sign/{token}/done` | EsignPublicController@done | esign/sign/done |
|
||||
|
||||
### 7.2 뷰 기술 스택
|
||||
|
||||
| 뷰 | 레이아웃 | 기술 |
|
||||
|-----|---------|------|
|
||||
| dashboard | layouts.app | React 18 + Babel |
|
||||
| create | layouts.app | React 18 + Babel |
|
||||
| detail | layouts.app | React 18 + Babel |
|
||||
| fields | layouts.app | React 18 + PDF.js + Babel |
|
||||
| send | layouts.app | React 18 + Babel |
|
||||
| sign/auth | Standalone HTML | React 18 + Babel |
|
||||
| sign/sign | Standalone HTML | React 18 + SignaturePad + Babel |
|
||||
| sign/done | Standalone HTML | React 18 + Babel |
|
||||
|
||||
**공통 패턴**:
|
||||
- 인증 화면: `@extends('layouts.app')` + HX-Redirect 패턴
|
||||
- 공개 화면: 독립 HTML (layouts.app 미사용, Tailwind CSS만 로드)
|
||||
- API 호출: `window.SAM_CONFIG?.apiBaseUrl` 또는 `config('services.api.base_url')`
|
||||
- 인증 토큰: `sessionStorage.getItem('api_access_token')` (인증), `esign_session_token` (공개)
|
||||
|
||||
### 7.3 화면별 기능
|
||||
|
||||
**대시보드** (`dashboard.blade.php`):
|
||||
- 상태별 통계 카드 (전체/진행중/대기/완료/만료)
|
||||
- 상태 필터, 검색, 날짜 범위 필터
|
||||
- 계약 목록 테이블 (페이지네이션)
|
||||
- 상태 배지 (색상 구분)
|
||||
|
||||
**계약 생성** (`create.blade.php`):
|
||||
- 계약 정보 입력 (제목, 설명, 서명 순서, 기한)
|
||||
- PDF 파일 업로드 (드래그&드롭)
|
||||
- 작성자 정보, 상대방 정보 입력
|
||||
- multipart/form-data 제출
|
||||
|
||||
**계약 상세** (`detail.blade.php`):
|
||||
- 계약 기본 정보
|
||||
- 서명자 현황 (작성자/상대방 상태, 서명 시각)
|
||||
- 감사 로그 타임라인
|
||||
- 액션 버튼 (발송, 리마인더, 취소, 다운로드, 검증)
|
||||
|
||||
**서명 위치 지정** (`fields.blade.php`):
|
||||
- PDF.js로 PDF 렌더링
|
||||
- 페이지 네비게이션
|
||||
- 서명자별 필드 추가 (클릭으로 위치 지정)
|
||||
- 필드 드래그/리사이즈
|
||||
- 필드 타입 설정 (서명/도장/텍스트/날짜/체크박스)
|
||||
- 좌표값 (% 기반)
|
||||
|
||||
**서명 요청 발송** (`send.blade.php`):
|
||||
- 발송 전 체크리스트 (서명 필드 설정 여부)
|
||||
- 서명 순서 확인
|
||||
- 최종 발송 버튼
|
||||
|
||||
**본인인증** (`sign/auth.blade.php`):
|
||||
- 계약 정보 표시 (제목, 서명자, 기한)
|
||||
- OTP 발송 버튼
|
||||
- 6자리 OTP 입력 폼
|
||||
- 재발송 기능
|
||||
|
||||
**서명 수행** (`sign/sign.blade.php`):
|
||||
- 3단계: 문서 확인 → 서명 입력 → 서명 확인
|
||||
- 문서 확인: PDF 다운로드 링크 + 동의 체크박스
|
||||
- 서명 입력: SignaturePad 캔버스 (터치/마우스)
|
||||
- 서명 확인: 미리보기 + 제출/다시서명 선택
|
||||
- 거절 기능 (사유 입력)
|
||||
|
||||
**서명 완료** (`sign/done.blade.php`):
|
||||
- 서명 완료/거절/기타 상태 분기 표시
|
||||
- 계약 정보, 서명자 이름, 서명 일시 표시
|
||||
|
||||
---
|
||||
|
||||
## 8. 핵심 플로우
|
||||
|
||||
### 8.1 계약 생성 → 발송
|
||||
|
||||
```
|
||||
[관리자]
|
||||
1. /esign/create 접속
|
||||
2. 계약 정보 + PDF 업로드 + 서명자 정보 입력
|
||||
3. POST /api/v1/esign/contracts
|
||||
→ EsignContractService::create()
|
||||
→ PDF 저장, SHA-256 해시, 계약 코드 생성
|
||||
→ 서명자 2인 생성 (access_token 128자)
|
||||
→ 상태: DRAFT
|
||||
4. /esign/{id}/fields 에서 서명 위치 지정
|
||||
→ POST /api/v1/esign/contracts/{id}/fields
|
||||
→ configureFields() 호출
|
||||
5. /esign/{id}/send 에서 발송 확인
|
||||
→ POST /api/v1/esign/contracts/{id}/send
|
||||
→ 상태: DRAFT → PENDING
|
||||
→ 첫 서명자: WAITING → NOTIFIED
|
||||
→ EsignRequestMail 이메일 발송
|
||||
```
|
||||
|
||||
### 8.2 서명 수행
|
||||
|
||||
```
|
||||
[서명자 B - 이메일 링크 클릭]
|
||||
1. /esign/sign/{token} 접속 → auth 화면
|
||||
2. GET /api/v1/esign/sign/{token} → 계약 정보 확인
|
||||
3. POST /api/v1/esign/sign/{token}/otp/send → OTP 발송
|
||||
4. POST /api/v1/esign/sign/{token}/otp/verify → OTP 검증
|
||||
→ sign_session_token 발급
|
||||
→ sessionStorage에 저장
|
||||
5. /esign/sign/{token}/sign 이동 → sign 화면
|
||||
6. 문서 확인 + 동의 체크 + 서명 입력 (SignaturePad)
|
||||
7. POST /api/v1/esign/sign/{token}/submit
|
||||
→ 서명 이미지 저장 (base64 → PNG 파일)
|
||||
→ 상태: AUTHENTICATED → SIGNED
|
||||
→ checkAndComplete() 호출
|
||||
8. /esign/sign/{token}/done 이동 → done 화면
|
||||
```
|
||||
|
||||
### 8.3 자동 완료 처리
|
||||
|
||||
```
|
||||
EsignSignService::checkAndComplete()
|
||||
1. 모든 서명자 상태 확인
|
||||
2. 전원 SIGNED → 계약 상태 COMPLETED + completed_at 설정
|
||||
3. 아직 미서명자 있음 → PARTIALLY_SIGNED
|
||||
→ 다음 서명 순서 서명자에게 이메일 발송
|
||||
→ 다음 서명자 상태: NOTIFIED
|
||||
```
|
||||
|
||||
### 8.4 상태 전이
|
||||
|
||||
**계약 상태**:
|
||||
```
|
||||
DRAFT → PENDING (send)
|
||||
PENDING → PARTIALLY_SIGNED (첫 서명 완료)
|
||||
PARTIALLY_SIGNED → COMPLETED (모든 서명 완료)
|
||||
DRAFT/PENDING → CANCELLED (관리자 취소)
|
||||
PENDING/PARTIALLY_SIGNED → REJECTED (서명자 거절)
|
||||
PENDING/PARTIALLY_SIGNED → EXPIRED (기한 초과, 추후 스케줄러)
|
||||
```
|
||||
|
||||
**서명자 상태**:
|
||||
```
|
||||
WAITING → NOTIFIED (이메일 발송)
|
||||
NOTIFIED → AUTHENTICATED (OTP 인증)
|
||||
AUTHENTICATED → SIGNED (서명 완료)
|
||||
NOTIFIED/AUTHENTICATED → REJECTED (거절)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 보안 설계
|
||||
|
||||
### 9.1 토큰 기반 접근
|
||||
|
||||
| 토큰 | 용도 | 생성 | 유효기간 |
|
||||
|------|------|------|---------|
|
||||
| access_token | 서명 링크 URL | `Str::random(128)` | 계약 만료일까지 |
|
||||
| sign_session_token | OTP 인증 후 세션 | `Str::random(64)` | 별도 관리 없음 |
|
||||
|
||||
### 9.2 OTP 인증
|
||||
|
||||
- 6자리 숫자 (`random_int(100000, 999999)`)
|
||||
- 5분 유효 (`otp_expires_at = now + 5min`)
|
||||
- 최대 5회 시도 (`otp_attempts`)
|
||||
- 초과 시 에러 반환 (토큰 재사용 불가 상태)
|
||||
|
||||
### 9.3 파일 무결성
|
||||
|
||||
- 업로드 시 SHA-256 해시 계산 → `original_file_hash`에 저장
|
||||
- 검증 시 `hash_equals()` 사용 (타이밍 공격 방지)
|
||||
- 서명 완료 PDF도 별도 해시 저장 (`signed_file_hash`)
|
||||
|
||||
### 9.4 멀티테넌트
|
||||
|
||||
- 모든 모델에 `BelongsToTenant` 트레이트 적용
|
||||
- 공개 서명 API에서는 `withoutGlobalScopes()` 사용
|
||||
- 감사 로그 기록 시 `logPublic()` 메서드로 tenant_id 명시
|
||||
|
||||
### 9.5 감사 추적
|
||||
|
||||
- 모든 주요 행위를 `esign_audit_logs`에 기록
|
||||
- IP 주소, User-Agent 자동 수집
|
||||
- 감사 로그는 삭제 불가 (`SoftDeletes` 미적용)
|
||||
|
||||
---
|
||||
|
||||
## 10. i18n (국제화)
|
||||
|
||||
### message 키 (`lang/ko/message.php` → `esign` 배열)
|
||||
|
||||
| 키 | 메시지 |
|
||||
|----|--------|
|
||||
| created | 전자계약이 생성되었습니다 |
|
||||
| cancelled | 전자계약이 취소되었습니다 |
|
||||
| sent | 서명 요청이 발송되었습니다 |
|
||||
| reminded | 리마인더가 발송되었습니다 |
|
||||
| fields_configured | 서명 필드가 설정되었습니다 |
|
||||
| otp_sent | 인증 코드가 발송되었습니다 |
|
||||
| otp_verified | 본인인증이 완료되었습니다 |
|
||||
| signed | 서명이 완료되었습니다 |
|
||||
| rejected | 서명이 거절되었습니다 |
|
||||
| completed | 전자계약이 완료되었습니다 |
|
||||
| verified | 문서 무결성이 확인되었습니다 |
|
||||
| downloaded | 문서가 다운로드되었습니다 |
|
||||
|
||||
### error 키 (`lang/ko/error.php` → `esign` 배열)
|
||||
|
||||
| 키 | 메시지 |
|
||||
|----|--------|
|
||||
| invalid_token | 유효하지 않은 서명 링크입니다 |
|
||||
| token_expired | 서명 링크가 만료되었습니다 |
|
||||
| contract_not_signable | 현재 서명할 수 없는 계약입니다 |
|
||||
| already_completed | 이미 완료된 계약입니다 |
|
||||
| already_cancelled | 이미 취소된 계약입니다 |
|
||||
| already_signed | 이미 서명이 완료되었습니다 |
|
||||
| invalid_status_for_send | 발송할 수 없는 상태입니다 |
|
||||
| no_sign_fields | 서명 필드가 설정되지 않았습니다 |
|
||||
| cannot_remind | 리마인더를 발송할 수 없는 상태입니다 |
|
||||
| fields_only_in_draft | 초안 상태에서만 필드를 설정할 수 있습니다 |
|
||||
| not_verified | 본인인증이 필요합니다 |
|
||||
| otp_max_attempts | 인증 시도 횟수를 초과했습니다 |
|
||||
| otp_not_sent | 인증 코드가 발송되지 않았습니다 |
|
||||
| otp_expired | 인증 코드가 만료되었습니다 |
|
||||
| otp_invalid | 인증 코드가 올바르지 않습니다 |
|
||||
| file_not_found | 파일을 찾을 수 없습니다 |
|
||||
|
||||
---
|
||||
|
||||
## 11. 재사용 패턴
|
||||
|
||||
### API 프로젝트
|
||||
|
||||
| 패턴 | 파일 | 용도 |
|
||||
|------|------|------|
|
||||
| Service 베이스 | `app/Services/Service.php` | tenantId(), apiUserId(), setContext() |
|
||||
| ApiResponse | `app/Helpers/ApiResponse.php` | handle() 패턴으로 일관된 응답 |
|
||||
| Auditable Trait | `app/Traits/Auditable.php` | created_by, updated_by, deleted_by 자동 관리 |
|
||||
| BelongsToTenant | `app/Traits/BelongsToTenant.php` | TenantScope 글로벌 스코프 |
|
||||
|
||||
### MNG 프로젝트
|
||||
|
||||
| 패턴 | 파일 | 용도 |
|
||||
|------|------|------|
|
||||
| 레이아웃 | `views/layouts/app.blade.php` | 사이드바 + 콘텐츠 영역 |
|
||||
| React 하이브리드 | `views/finance/journal-entries.blade.php` | CDN React + Babel 패턴 참고 |
|
||||
| HX-Redirect | 컨트롤러 패턴 | HTMX 부분 로드 시 전체 리로드 |
|
||||
| SidebarMenu | `app/Services/SidebarMenuService.php` | DB 기반 메뉴 |
|
||||
|
||||
---
|
||||
|
||||
## 12. 추후 구현 예정
|
||||
|
||||
| 항목 | 우선순위 | 설명 |
|
||||
|------|---------|------|
|
||||
| PDF 합성 (FPDI) | 높음 | 원본 PDF에 서명 이미지 오버레이 |
|
||||
| 감사 증적 페이지 | 높음 | 완료 PDF 마지막에 감사 정보 페이지 추가 |
|
||||
| 파일 암호화 (AES-256) | 중간 | 원본 PDF 암호화 저장 |
|
||||
| 만료 자동 처리 | 중간 | 스케줄러로 expires_at 초과 계약 expired 처리 |
|
||||
| 리마인더 자동 발송 | 낮음 | 만료 3일 전 자동 리마인드 |
|
||||
| SMS OTP | 낮음 | 이메일 외 SMS 인증 지원 |
|
||||
| OTP bcrypt 해싱 | 중간 | 현재 평문 저장 → bcrypt 해싱 |
|
||||
|
||||
---
|
||||
|
||||
## 13. 메뉴 추가 안내
|
||||
|
||||
메뉴 시더 실행 금지 규칙에 따라, 아래 tinker 명령어를 사용자가 직접 실행해야 합니다:
|
||||
|
||||
```bash
|
||||
# 1. 상위 메뉴 "전자계약 (E-Sign)" 추가
|
||||
ssh sam-server "cd /home/webservice/mng && php artisan tinker --execute=\"
|
||||
\\\$menu = App\\\\Models\\\\Commons\\\\Menu::create([
|
||||
'tenant_id' => 1,
|
||||
'parent_id' => null,
|
||||
'name' => '전자계약',
|
||||
'url' => '/esign',
|
||||
'icon' => 'ri-quill-pen-line',
|
||||
'sort_order' => 25,
|
||||
'is_active' => true,
|
||||
]);
|
||||
echo 'Created menu ID: ' . \\\$menu->id;
|
||||
\""
|
||||
|
||||
# 2. 하위 메뉴 추가 (parent_id를 위에서 생성된 ID로 교체)
|
||||
ssh sam-server "cd /home/webservice/mng && php artisan tinker --execute=\"
|
||||
App\\\\Models\\\\Commons\\\\Menu::create([
|
||||
'tenant_id' => 1,
|
||||
'parent_id' => <상위메뉴ID>,
|
||||
'name' => '계약 대시보드',
|
||||
'url' => '/esign',
|
||||
'icon' => 'ri-dashboard-line',
|
||||
'sort_order' => 1,
|
||||
'is_active' => true,
|
||||
]);
|
||||
App\\\\Models\\\\Commons\\\\Menu::create([
|
||||
'tenant_id' => 1,
|
||||
'parent_id' => <상위메뉴ID>,
|
||||
'name' => '새 계약 생성',
|
||||
'url' => '/esign/create',
|
||||
'icon' => 'ri-add-line',
|
||||
'sort_order' => 2,
|
||||
'is_active' => true,
|
||||
]);
|
||||
\""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*이 문서는 SAM E-Sign v1.0 구현 결과를 기록한 것입니다. 추후 기능 추가 시 업데이트됩니다.*
|
||||
@@ -1,7 +1,7 @@
|
||||
# 프로젝트 문서 인덱스
|
||||
|
||||
> SAM 시스템 개발 프로젝트별 문서 모음
|
||||
> **최종 업데이트**: 2025-12-23
|
||||
> **최종 업데이트**: 2026-02-12
|
||||
|
||||
---
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
| [mng-mobile-responsive](#mng-mobile-responsive---모바일-반응형) | 🟡 진행중 | mng 모바일 반응형 개선 |
|
||||
| [auto-login](#auto-login---자동-로그인) | ⚪ 대기 | 자동 로그인 기능 |
|
||||
| [migration-5130-mng](#migration-5130-mng---5130--mng-마이그레이션) | 🟡 진행중 | 5130 → mng 통합 마이그레이션 |
|
||||
| [e-sign](#e-sign---전자계약-서명) | 🟢 v1.0 구현 완료 | 전자계약 서명 솔루션 (SAM E-Sign) |
|
||||
|
||||
---
|
||||
|
||||
@@ -175,6 +176,34 @@
|
||||
|
||||
---
|
||||
|
||||
### e-sign - 전자계약 서명
|
||||
|
||||
**경로**: `docs/projects/e-sign/`
|
||||
**상태**: 🟢 v1.0 구현 완료 (2026-02-12)
|
||||
**목표**: 모두싸인과 유사한 간편 전자계약 서명 솔루션 자체 구축
|
||||
|
||||
**핵심 문서**:
|
||||
- [technical-design.md](./e-sign/technical-design.md) - 기술 설계 문서
|
||||
- [implementation-guide.md](./e-sign/implementation-guide.md) - 구현 가이드
|
||||
|
||||
**구현 범위**:
|
||||
| 영역 | 수량 |
|
||||
|------|------|
|
||||
| DB 마이그레이션 | 4개 (esign_contracts, esign_signers, esign_sign_fields, esign_audit_logs) |
|
||||
| API 모델 | 4개 |
|
||||
| API 서비스 | 4개 |
|
||||
| API 컨트롤러 | 2개 (16 엔드포인트) |
|
||||
| MNG 컨트롤러 | 2개 (8 화면) |
|
||||
| MNG 뷰 | 8개 (React 하이브리드) |
|
||||
|
||||
**기술 스택**: Laravel 11 + React 18 + SignaturePad + PDF.js
|
||||
|
||||
**참고 자료**:
|
||||
- `esign-storyboard.pptx` - 화면 스토리보드
|
||||
- `storyboard-config.json` - 스토리보드 설정
|
||||
|
||||
---
|
||||
|
||||
## 디렉토리 구조
|
||||
|
||||
```
|
||||
@@ -202,7 +231,8 @@ docs/projects/
|
||||
├── legacy-5130/ # 레거시 분석 (참조용)
|
||||
├── mng-mobile-responsive/ # 모바일 반응형
|
||||
├── auto-login/ # 자동 로그인
|
||||
└── migration-5130-mng/ # 5130→mng 마이그레이션
|
||||
├── migration-5130-mng/ # 5130→mng 마이그레이션
|
||||
└── e-sign/ # 전자계약 서명 (SAM E-Sign)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user