diff --git a/projects/e-sign/implementation-guide.md b/projects/e-sign/implementation-guide.md new file mode 100644 index 0000000..ac8b7b1 --- /dev/null +++ b/projects/e-sign/implementation-guide.md @@ -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 구현 결과를 기록한 것입니다. 추후 기능 추가 시 업데이트됩니다.* diff --git a/projects/index_projects.md b/projects/index_projects.md index 9bb7c01..0f2f71d 100644 --- a/projects/index_projects.md +++ b/projects/index_projects.md @@ -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) ``` ---