# SAM E-Sign API 명세서 > 버전: 1.0.0 > 최종 수정: 2026-02-12 > 작성자: IT 혁신팀 --- ## 목차 1. [개요](#1-개요) 2. [인증](#2-인증) 3. [공통 규칙](#3-공통-규칙) 4. [계약 관리 API](#4-계약-관리-api) 5. [서명 프로세스 API](#5-서명-프로세스-api) 6. [데이터 모델](#6-데이터-모델) 7. [에러 코드](#7-에러-코드) 8. [워크플로우](#8-워크플로우) 9. [이메일 알림](#9-이메일-알림) 10. [보안](#10-보안) 11. [부록: Enum 값 정리](#11-부록-enum-값-정리) --- ## 1. 개요 ### 1.1 기본 정보 | 항목 | 값 | |------|-----| | Base URL | `https://api.sam.kr/api/v1/esign` | | 프로토콜 | HTTPS | | 데이터 형식 | JSON (multipart/form-data는 파일 업로드 시) | | 문자 인코딩 | UTF-8 | | 인증 방식 | API Key + Bearer Token / Access Token | ### 1.2 API 그룹 | 그룹 | 경로 접두사 | 인증 | 엔드포인트 수 | 대상 | |------|-----------|------|-------------|------| | 계약 관리 | `/api/v1/esign/contracts` | API Key + Bearer Token | 10개 | MNG 관리자 | | 서명 프로세스 | `/api/v1/esign/sign/{token}` | API Key + Access Token | 6개 | 외부 서명자 | ### 1.3 엔드포인트 요약 #### 계약 관리 API (인증 필요) | # | Method | Path | 설명 | |---|--------|------|------| | 1 | GET | `/contracts` | 계약 목록 조회 | | 2 | GET | `/contracts/stats` | 상태별 통계 조회 | | 3 | GET | `/contracts/{id}` | 계약 상세 조회 | | 4 | POST | `/contracts` | 계약 생성 | | 5 | POST | `/contracts/{id}/fields` | 서명 필드 설정 | | 6 | POST | `/contracts/{id}/send` | 서명 요청 발송 | | 7 | POST | `/contracts/{id}/remind` | 리마인더 발송 | | 8 | POST | `/contracts/{id}/cancel` | 계약 취소 | | 9 | GET | `/contracts/{id}/download` | PDF 다운로드 | | 10 | GET | `/contracts/{id}/verify` | 무결성 검증 | #### 서명 프로세스 API (토큰 기반) | # | Method | Path | 설명 | |---|--------|------|------| | 1 | GET | `/sign/{token}` | 계약 정보 조회 | | 2 | POST | `/sign/{token}/otp/send` | OTP 발송 | | 3 | POST | `/sign/{token}/otp/verify` | OTP 인증 | | 4 | GET | `/sign/{token}/document` | 계약 문서 조회 | | 5 | POST | `/sign/{token}/submit` | 서명 제출 | | 6 | POST | `/sign/{token}/reject` | 서명 거절 | --- ## 2. 인증 ### 2.1 API Key 인증 모든 API 요청에 API Key가 필요합니다. ```http X-API-Key: {api-key} ``` - 미들웨어: `auth.apikey` - API Key가 없거나 유효하지 않으면 `401 Unauthorized` 반환 ### 2.2 Bearer Token 인증 (계약 관리 API) 계약 관리 API는 추가로 Bearer Token이 필요합니다. ```http Authorization: Bearer {sanctum-token} ``` - 미들웨어: `auth:sanctum` - Laravel Sanctum 기반 - 로그인 후 발급되는 Personal Access Token 사용 ### 2.3 Access Token 인증 (서명 프로세스 API) 서명 프로세스 API는 URL 경로에 포함된 토큰으로 인증합니다. ``` /api/v1/esign/sign/{access_token} ``` - 128자 랜덤 문자열 (서명자별 고유) - 계약 발송 시 자동 생성 - `token_expires_at` 이후 만료 --- ## 3. 공통 규칙 ### 3.1 응답 형식 #### 성공 응답 ```json { "success": true, "message": "요청 성공 메시지", "data": { ... } } ``` #### 에러 응답 ```json { "success": false, "message": "[에러코드] 에러 메시지", "error": { "code": 404, "details": null } } ``` #### 유효성 검증 에러 (422) ```json { "message": "The title field is required.", "errors": { "title": ["The title field is required."], "file": ["The file must be a file of type: pdf."] } } ``` ### 3.2 페이지네이션 목록 조회 API는 Laravel 표준 페이지네이션 형식을 사용합니다. ```json { "success": true, "data": { "current_page": 1, "data": [ ... ], "first_page_url": "...?page=1", "from": 1, "last_page": 5, "last_page_url": "...?page=5", "links": [ ... ], "next_page_url": "...?page=2", "path": "...", "per_page": 20, "prev_page_url": null, "to": 20, "total": 100 } } ``` ### 3.3 Multi-Tenant - 모든 데이터는 `tenant_id`로 격리됩니다 - `BelongsToTenant` 글로벌 스코프가 자동으로 필터링합니다 - 다른 테넌트의 데이터에는 접근할 수 없습니다 ### 3.4 Soft Delete - 계약(`esign_contracts`)은 Soft Delete를 지원합니다 - 삭제된 데이터는 `deleted_at` 필드에 삭제 시각이 기록됩니다 - 감사 로그(`esign_audit_logs`)는 삭제 불가능합니다 --- ## 4. 계약 관리 API > 미들웨어: `auth.apikey`, `auth:sanctum` > 경로 접두사: `/api/v1/esign/contracts` ### 4.1 계약 목록 조회 ``` GET /api/v1/esign/contracts ``` 계약 목록을 페이지네이션으로 조회합니다. 필터 및 검색을 지원합니다. **Query Parameters** | 파라미터 | 타입 | 필수 | 기본값 | 설명 | |---------|------|------|--------|------| | `status` | string | - | - | 계약 상태 필터 | | `search` | string | - | - | 제목 또는 계약 코드 검색 | | `date_from` | date | - | - | 생성일 시작 (YYYY-MM-DD) | | `date_to` | date | - | - | 생성일 종료 (YYYY-MM-DD) | | `per_page` | integer | - | 20 | 페이지당 항목 수 | **Request** ```http GET /api/v1/esign/contracts?status=pending&search=공급&per_page=10 HTTP/1.1 X-API-Key: {api-key} Authorization: Bearer {token} ``` **Response (200 OK)** ```json { "success": true, "message": "데이터 조회 완료", "data": { "current_page": 1, "data": [ { "id": 1, "tenant_id": 1, "contract_code": "ES-20260212-A1B2C3", "title": "제품 공급 계약서", "description": "2026년 제품 공급 계약", "sign_order_type": "counterpart_first", "original_file_name": "supply-contract.pdf", "original_file_size": 102400, "status": "pending", "expires_at": "2026-02-26T00:00:00.000000Z", "completed_at": null, "created_by": 1, "created_at": "2026-02-12T10:00:00.000000Z", "updated_at": "2026-02-12T10:30:00.000000Z", "signers": [ { "id": 1, "name": "김철수", "email": "kim@example.com", "role": "counterpart", "status": "notified", "signed_at": null }, { "id": 2, "name": "이영희", "email": "lee@company.com", "role": "creator", "status": "waiting", "signed_at": null } ], "creator": { "id": 1, "name": "관리자" } } ], "per_page": 10, "total": 25 } } ``` --- ### 4.2 상태별 통계 조회 ``` GET /api/v1/esign/contracts/stats ``` 계약 상태별 건수를 집계합니다. **Request** ```http GET /api/v1/esign/contracts/stats HTTP/1.1 X-API-Key: {api-key} Authorization: Bearer {token} ``` **Response (200 OK)** ```json { "success": true, "message": "데이터 조회 완료", "data": { "total": 150, "draft": 10, "pending": 25, "partially_signed": 15, "completed": 80, "expired": 5, "cancelled": 10, "rejected": 5 } } ``` --- ### 4.3 계약 상세 조회 ``` GET /api/v1/esign/contracts/{id} ``` 계약의 상세 정보를 서명자, 서명 필드, 감사 로그와 함께 조회합니다. **Path Parameters** | 파라미터 | 타입 | 설명 | |---------|------|------| | `id` | integer | 계약 ID | **Request** ```http GET /api/v1/esign/contracts/1 HTTP/1.1 X-API-Key: {api-key} Authorization: Bearer {token} ``` **Response (200 OK)** ```json { "success": true, "message": "데이터 조회 완료", "data": { "id": 1, "tenant_id": 1, "contract_code": "ES-20260212-A1B2C3", "title": "제품 공급 계약서", "description": "2026년 제품 공급 계약", "sign_order_type": "counterpart_first", "original_file_path": "esign/1/originals/abc123.pdf", "original_file_name": "supply-contract.pdf", "original_file_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "original_file_size": 102400, "signed_file_path": null, "signed_file_hash": null, "status": "pending", "expires_at": "2026-02-26T00:00:00.000000Z", "completed_at": null, "created_by": 1, "updated_by": 1, "created_at": "2026-02-12T10:00:00.000000Z", "updated_at": "2026-02-12T10:30:00.000000Z", "signers": [ { "id": 1, "tenant_id": 1, "contract_id": 1, "role": "counterpart", "sign_order": 1, "name": "김철수", "email": "kim@example.com", "phone": "010-1234-5678", "token_expires_at": "2026-02-26T00:00:00.000000Z", "otp_attempts": 0, "auth_verified_at": null, "signature_image_path": null, "signed_at": null, "consent_agreed_at": null, "sign_ip_address": null, "sign_user_agent": null, "status": "notified", "rejected_reason": null }, { "id": 2, "tenant_id": 1, "contract_id": 1, "role": "creator", "sign_order": 2, "name": "이영희", "email": "lee@company.com", "phone": "010-9876-5432", "status": "waiting" } ], "sign_fields": [ { "id": 1, "contract_id": 1, "signer_id": 1, "page_number": 1, "position_x": "10.50", "position_y": "80.25", "width": "20.00", "height": "10.00", "field_type": "signature", "field_label": "상대방 서명", "field_value": null, "is_required": true, "sort_order": 0 } ], "audit_logs": [ { "id": 2, "contract_id": 1, "signer_id": null, "action": "sent", "ip_address": "192.168.1.100", "user_agent": "Mozilla/5.0...", "metadata": null, "created_at": "2026-02-12T10:30:00.000000Z" }, { "id": 1, "contract_id": 1, "signer_id": null, "action": "created", "ip_address": "192.168.1.100", "user_agent": "Mozilla/5.0...", "metadata": null, "created_at": "2026-02-12T10:00:00.000000Z" } ], "creator": { "id": 1, "name": "관리자" } } } ``` **Error (404)** ```json { "success": false, "message": "[404] 데이터를 찾을 수 없습니다", "error": { "code": 404, "details": null } } ``` --- ### 4.4 계약 생성 ``` POST /api/v1/esign/contracts ``` 새 전자계약을 생성합니다. PDF 파일 업로드와 함께 작성자/상대방 정보를 등록합니다. **Content-Type**: `multipart/form-data` **Request Body** | 필드 | 타입 | 필수 | 검증 규칙 | 설명 | |------|------|------|----------|------| | `title` | string | O | max:200 | 계약 제목 | | `description` | string | - | max:2000 | 계약 설명 | | `sign_order_type` | string | - | in:counterpart_first,creator_first | 서명 순서 (기본: counterpart_first) | | `file` | file | O | mimes:pdf, max:20480 | PDF 파일 (최대 20MB) | | `expires_at` | date | - | after:now | 만료일 (기본: 14일 후) | | `creator_name` | string | O | max:100 | 작성자 이름 | | `creator_email` | string | O | email, max:255 | 작성자 이메일 | | `creator_phone` | string | - | max:20 | 작성자 전화번호 | | `counterpart_name` | string | O | max:100 | 상대방 이름 | | `counterpart_email` | string | O | email, max:255 | 상대방 이메일 | | `counterpart_phone` | string | - | max:20 | 상대방 전화번호 | **Request (cURL)** ```bash curl -X POST https://api.sam.kr/api/v1/esign/contracts \ -H "X-API-Key: {api-key}" \ -H "Authorization: Bearer {token}" \ -F "title=제품 공급 계약서" \ -F "description=2026년 제품 공급 계약" \ -F "sign_order_type=counterpart_first" \ -F "file=@/path/to/contract.pdf" \ -F "expires_at=2026-02-26" \ -F "creator_name=이영희" \ -F "creator_email=lee@company.com" \ -F "creator_phone=010-9876-5432" \ -F "counterpart_name=김철수" \ -F "counterpart_email=kim@example.com" \ -F "counterpart_phone=010-1234-5678" ``` **Response (200 OK)** ```json { "success": true, "message": "계약이 생성되었습니다", "data": { "id": 1, "contract_code": "ES-20260212-A1B2C3", "title": "제품 공급 계약서", "description": "2026년 제품 공급 계약", "sign_order_type": "counterpart_first", "original_file_name": "contract.pdf", "original_file_hash": "e3b0c44298fc1c14...", "original_file_size": 102400, "status": "draft", "expires_at": "2026-02-26T00:00:00.000000Z", "created_by": 1, "signers": [ { "id": 1, "role": "counterpart", "sign_order": 1, "name": "김철수", "email": "kim@example.com", "status": "waiting" }, { "id": 2, "role": "creator", "sign_order": 2, "name": "이영희", "email": "lee@company.com", "status": "waiting" } ] } } ``` **Error (422 Validation)** ```json { "message": "The title field is required. (and 2 more errors)", "errors": { "title": ["The title field is required."], "file": ["The file must be a file of type: pdf."], "counterpart_email": ["The counterpart email must be a valid email address."] } } ``` **처리 순서**: 1. PDF 파일을 `storage/app/esign/{tenant_id}/originals/` 에 저장 2. SHA-256 해시 생성 및 저장 3. 계약 코드 자동 생성 (`ES-YYYYMMDD-XXXXXX`) 4. 서명자 2명 생성 (creator, counterpart) 5. 각 서명자에게 128자 access_token 발급 6. 감사 로그 기록 (`created`) --- ### 4.5 서명 필드 설정 ``` POST /api/v1/esign/contracts/{id}/fields ``` PDF 문서 위에 서명 위치를 설정합니다. 기존 필드는 삭제 후 재생성됩니다. **Content-Type**: `application/json` **Path Parameters** | 파라미터 | 타입 | 설명 | |---------|------|------| | `id` | integer | 계약 ID | **Request Body** | 필드 | 타입 | 필수 | 검증 규칙 | 설명 | |------|------|------|----------|------| | `fields` | array | O | min:1 | 서명 필드 배열 | | `fields.*.signer_id` | integer | O | exists:esign_signers,id | 서명자 ID | | `fields.*.page_number` | integer | O | min:1 | 페이지 번호 | | `fields.*.position_x` | numeric | O | min:0, max:100 | X 좌표 (%) | | `fields.*.position_y` | numeric | O | min:0, max:100 | Y 좌표 (%) | | `fields.*.width` | numeric | O | min:1, max:100 | 너비 (%) | | `fields.*.height` | numeric | O | min:1, max:100 | 높이 (%) | | `fields.*.field_type` | string | - | in:signature,stamp,text,date,checkbox | 유형 (기본: signature) | | `fields.*.field_label` | string | - | max:100 | 라벨 | | `fields.*.is_required` | boolean | - | - | 필수 여부 (기본: true) | | `fields.*.sort_order` | integer | - | min:0 | 정렬 순서 (기본: 0) | **Request** ```json POST /api/v1/esign/contracts/1/fields { "fields": [ { "signer_id": 1, "page_number": 3, "position_x": 10.5, "position_y": 80.25, "width": 20.0, "height": 10.0, "field_type": "signature", "field_label": "상대방 서명", "is_required": true, "sort_order": 0 }, { "signer_id": 2, "page_number": 3, "position_x": 60.0, "position_y": 80.25, "width": 20.0, "height": 10.0, "field_type": "signature", "field_label": "작성자 서명", "is_required": true, "sort_order": 1 } ] } ``` **Response (200 OK)** ```json { "success": true, "message": "서명 필드가 설정되었습니다", "data": { "id": 1, "contract_code": "ES-20260212-A1B2C3", "status": "draft", "sign_fields": [ { "id": 1, "signer_id": 1, "page_number": 3, "position_x": "10.50", "position_y": "80.25", "width": "20.00", "height": "10.00", "field_type": "signature", "field_label": "상대방 서명", "is_required": true, "sort_order": 0 }, { "id": 2, "signer_id": 2, "page_number": 3, "position_x": "60.00", "position_y": "80.25", "width": "20.00", "height": "10.00", "field_type": "signature", "field_label": "작성자 서명", "is_required": true, "sort_order": 1 } ] } } ``` **에러 조건**: - `404`: 계약을 찾을 수 없음 - `400`: `draft` 상태가 아닌 계약 → 필드 설정 불가 **좌표 체계**: - 모든 좌표는 **백분율(%)** 기준 (0~100) - `position_x`: 페이지 왼쪽 기준 가로 위치 - `position_y`: 페이지 상단 기준 세로 위치 - `width`, `height`: 페이지 대비 크기 --- ### 4.6 서명 요청 발송 ``` POST /api/v1/esign/contracts/{id}/send ``` 계약을 발송하고 첫 번째 서명자에게 이메일을 보냅니다. **Path Parameters** | 파라미터 | 타입 | 설명 | |---------|------|------| | `id` | integer | 계약 ID | **Request** ```http POST /api/v1/esign/contracts/1/send HTTP/1.1 X-API-Key: {api-key} Authorization: Bearer {token} ``` **Response (200 OK)** ```json { "success": true, "message": "계약이 발송되었습니다", "data": { "id": 1, "contract_code": "ES-20260212-A1B2C3", "status": "pending", "signers": [ { "id": 1, "status": "notified", "email": "kim@example.com" } ] } } ``` **전제 조건**: - 계약 상태가 `draft`여야 함 - 서명 필드가 최소 1개 이상 설정되어 있어야 함 **처리 순서**: 1. 계약 상태: `draft` → `pending` 2. 첫 번째 서명자(sign_order=1) 상태: `waiting` → `notified` 3. 첫 번째 서명자에게 `EsignRequestMail` 발송 4. 감사 로그 기록 (`sent`) --- ### 4.7 리마인더 발송 ``` POST /api/v1/esign/contracts/{id}/remind ``` 아직 서명하지 않은 서명자에게 알림 이메일을 재발송합니다. **Path Parameters** | 파라미터 | 타입 | 설명 | |---------|------|------| | `id` | integer | 계약 ID | **Response (200 OK)** ```json { "success": true, "message": "서명 알림이 재발송되었습니다", "data": { "id": 1, "contract_code": "ES-20260212-A1B2C3", "status": "pending" } } ``` **전제 조건**: - 계약 상태가 `pending` 또는 `partially_signed`여야 함 --- ### 4.8 계약 취소 ``` POST /api/v1/esign/contracts/{id}/cancel ``` 진행 중인 계약을 취소합니다. **Path Parameters** | 파라미터 | 타입 | 설명 | |---------|------|------| | `id` | integer | 계약 ID | **Response (200 OK)** ```json { "success": true, "message": "계약이 취소되었습니다", "data": { "id": 1, "contract_code": "ES-20260212-A1B2C3", "status": "cancelled" } } ``` **에러 조건**: - `400`: 이미 `completed` 또는 `cancelled` 상태인 계약 --- ### 4.9 계약 파일 다운로드 ``` GET /api/v1/esign/contracts/{id}/download ``` 계약 PDF 파일을 다운로드합니다. 서명 완료 시 서명된 파일, 미완료 시 원본을 반환합니다. **Path Parameters** | 파라미터 | 타입 | 설명 | |---------|------|------| | `id` | integer | 계약 ID | **Response (200 OK)** ``` Content-Type: application/pdf Content-Disposition: attachment; filename="supply-contract.pdf" ``` **에러 조건**: - `404`: 계약 또는 파일을 찾을 수 없음 --- ### 4.10 무결성 검증 ``` GET /api/v1/esign/contracts/{id}/verify ``` 저장된 해시와 현재 파일의 해시를 비교하여 문서 변조 여부를 확인합니다. **Path Parameters** | 파라미터 | 타입 | 설명 | |---------|------|------| | `id` | integer | 계약 ID | **Response (200 OK)** ```json { "success": true, "message": "무결성 검증 완료", "data": { "verified": true, "original_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" } } ``` 변조가 감지된 경우: ```json { "success": true, "message": "무결성 검증 완료", "data": { "verified": false, "original_hash": "e3b0c44298fc1c14..." } } ``` --- ## 5. 서명 프로세스 API > 미들웨어: `auth.apikey` > 경로 접두사: `/api/v1/esign/sign/{token}` > 인증: Access Token (URL 경로 포함) ### 5.1 계약 정보 조회 ``` GET /api/v1/esign/sign/{token} ``` 토큰으로 계약 및 서명자 정보를 조회합니다. 서명자가 이메일 링크를 클릭하면 호출됩니다. **Path Parameters** | 파라미터 | 타입 | 설명 | |---------|------|------| | `token` | string | 서명자 Access Token (128자) | **Response (200 OK)** ```json { "success": true, "message": "데이터 조회 완료", "data": { "contract": { "id": 1, "contract_code": "ES-20260212-A1B2C3", "title": "제품 공급 계약서", "description": "2026년 제품 공급 계약", "status": "pending", "expires_at": "2026-02-26T00:00:00.000000Z", "signers": [ { "id": 1, "name": "김철수", "role": "counterpart", "status": "notified", "signed_at": null } ] }, "signer": { "id": 1, "role": "counterpart", "sign_order": 1, "name": "김철수", "email": "kim@example.com", "phone": "010-1234-5678", "status": "notified", "auth_verified_at": null, "signed_at": null } } } ``` **에러 조건**: - `404`: 유효하지 않은 토큰 - `400`: 토큰 만료 --- ### 5.2 OTP 발송 ``` POST /api/v1/esign/sign/{token}/otp/send ``` 서명자 이메일로 6자리 OTP 코드를 발송합니다. **Path Parameters** | 파라미터 | 타입 | 설명 | |---------|------|------| | `token` | string | 서명자 Access Token | **Response (200 OK)** ```json { "success": true, "message": "OTP가 발송되었습니다", "data": { "message": "OTP가 발송되었습니다" } } ``` **참고**: - OTP: 6자리 숫자 (100000~999999) - 유효 시간: 5분 - 최대 시도 횟수: 5회 - 재발송 시 기존 OTP는 무효화되고 새 OTP 발급 - 개발 환경: OTP가 로그에 출력됨 --- ### 5.3 OTP 인증 ``` POST /api/v1/esign/sign/{token}/otp/verify ``` 발송된 OTP를 검증하고 세션 토큰을 발급합니다. **Path Parameters** | 파라미터 | 타입 | 설명 | |---------|------|------| | `token` | string | 서명자 Access Token | **Request Body** | 필드 | 타입 | 필수 | 검증 규칙 | 설명 | |------|------|------|----------|------| | `otp_code` | string | O | size:6 | OTP 코드 | **Request** ```json { "otp_code": "482637" } ``` **Response (200 OK)** ```json { "success": true, "message": "OTP 인증이 완료되었습니다", "data": { "sign_session_token": "a3f8b2c1d4e5f6...", "signer": { "id": 1, "status": "authenticated", "auth_verified_at": "2026-02-12T11:00:00.000000Z" } } } ``` **에러 조건**: | 상태 코드 | 조건 | 메시지 | |----------|------|--------| | 400 | OTP 미발송 | OTP가 발송되지 않았습니다 | | 400 | OTP 만료 (5분 초과) | OTP가 만료되었습니다 | | 400 | OTP 불일치 | OTP가 일치하지 않습니다 | | 400 | 시도 횟수 초과 (5회) | OTP 최대 시도 횟수를 초과했습니다 | --- ### 5.4 계약 문서 조회 ``` GET /api/v1/esign/sign/{token}/document ``` 계약 PDF를 브라우저에서 표시할 수 있도록 스트리밍합니다. **Path Parameters** | 파라미터 | 타입 | 설명 | |---------|------|------| | `token` | string | 서명자 Access Token | **Response (200 OK)** ``` Content-Type: application/pdf ``` **참고**: `Content-Disposition` 헤더 없음 (브라우저 인라인 표시) --- ### 5.5 서명 제출 ``` POST /api/v1/esign/sign/{token}/submit ``` 서명 이미지를 제출하고 서명을 완료합니다. **Path Parameters** | 파라미터 | 타입 | 설명 | |---------|------|------| | `token` | string | 서명자 Access Token | **Request Body** | 필드 | 타입 | 필수 | 설명 | |------|------|------|------| | `signature_image` | string | O | Base64 인코딩된 서명 이미지 (PNG) | **Request** ```json { "signature_image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..." } ``` **Response (200 OK)** ```json { "success": true, "message": "서명이 완료되었습니다", "data": { "id": 1, "contract_id": 1, "name": "김철수", "status": "signed", "signed_at": "2026-02-12T11:05:00.000000Z", "consent_agreed_at": "2026-02-12T11:05:00.000000Z", "sign_ip_address": "203.246.xx.xx", "signature_image_path": "esign/1/signatures/1_1.png" } } ``` **전제 조건**: - OTP 인증 완료 필수 (`auth_verified_at` 존재) - 아직 서명하지 않은 상태여야 함 - 계약이 서명 가능 상태여야 함 (pending 또는 partially_signed) **처리 순서**: 1. Base64 이미지 → PNG 파일로 저장 (`esign/{tenant_id}/signatures/{contract_id}_{signer_id}.png`) 2. 서명자 상태: `authenticated` → `signed` 3. `signed_at`, `consent_agreed_at`, IP, User Agent 기록 4. 감사 로그 기록 (`signed`) 5. 모든 서명자 완료 여부 확인 → 완료 시 계약 상태: `completed` 6. 순차 서명인 경우 다음 서명자에게 이메일 발송 --- ### 5.6 서명 거절 ``` POST /api/v1/esign/sign/{token}/reject ``` 서명을 거절합니다. 계약 전체가 거절 상태로 변경됩니다. **Path Parameters** | 파라미터 | 타입 | 설명 | |---------|------|------| | `token` | string | 서명자 Access Token | **Request Body** | 필드 | 타입 | 필수 | 검증 규칙 | 설명 | |------|------|------|----------|------| | `reason` | string | O | max:1000 | 거절 사유 | **Request** ```json { "reason": "계약 조건에 대해 추가 협의가 필요합니다." } ``` **Response (200 OK)** ```json { "success": true, "message": "서명이 거절되었습니다", "data": { "id": 1, "contract_id": 1, "name": "김철수", "status": "rejected", "rejected_reason": "계약 조건에 대해 추가 협의가 필요합니다." } } ``` **처리 순서**: 1. 서명자 상태: → `rejected` 2. `rejected_reason` 기록 3. 계약 상태: → `rejected` 4. 감사 로그 기록 (`rejected`) --- ## 6. 데이터 모델 ### 6.1 esign_contracts (계약) | 컬럼 | 타입 | NULL | 기본값 | 설명 | |------|------|------|--------|------| | id | bigint unsigned | NO | AUTO | PK | | tenant_id | bigint unsigned | NO | - | 테넌트 ID (FK) | | contract_code | varchar(50) | NO | - | 계약 코드 (UNIQUE) | | title | varchar(200) | NO | - | 계약 제목 | | description | text | YES | NULL | 계약 설명 | | sign_order_type | enum | NO | counterpart_first | 서명 순서 | | original_file_path | varchar(500) | NO | - | 원본 PDF 경로 | | original_file_name | varchar(255) | NO | - | 원본 파일명 | | original_file_hash | varchar(64) | NO | - | SHA-256 해시 | | original_file_size | int unsigned | NO | - | 파일 크기 (bytes) | | signed_file_path | varchar(500) | YES | NULL | 서명 PDF 경로 | | signed_file_hash | varchar(64) | YES | NULL | 서명 파일 해시 | | status | enum | NO | draft | 계약 상태 | | expires_at | datetime | YES | NULL | 만료일시 | | completed_at | datetime | YES | NULL | 완료일시 | | created_by | bigint unsigned | NO | - | 생성자 ID | | updated_by | bigint unsigned | NO | - | 수정자 ID | | deleted_by | bigint unsigned | YES | NULL | 삭제자 ID | | created_at | timestamp | NO | CURRENT | 생성일시 | | updated_at | timestamp | NO | CURRENT | 수정일시 | | deleted_at | timestamp | YES | NULL | 삭제일시 | **인덱스**: `contract_code` (UNIQUE), `tenant_id` + `status` (INDEX) ### 6.2 esign_signers (서명자) | 컬럼 | 타입 | NULL | 기본값 | 설명 | |------|------|------|--------|------| | id | bigint unsigned | NO | AUTO | PK | | tenant_id | bigint unsigned | NO | - | 테넌트 ID | | contract_id | bigint unsigned | NO | - | 계약 ID (FK) | | role | enum | NO | - | 역할 (creator/counterpart) | | sign_order | tinyint unsigned | NO | - | 서명 순서 | | name | varchar(100) | NO | - | 이름 | | email | varchar(255) | NO | - | 이메일 | | phone | varchar(20) | YES | NULL | 전화번호 | | access_token | varchar(128) | NO | - | 액세스 토큰 (UNIQUE) | | token_expires_at | datetime | YES | NULL | 토큰 만료일시 | | otp_code | varchar(6) | YES | NULL | OTP 코드 | | otp_expires_at | datetime | YES | NULL | OTP 만료일시 | | otp_attempts | tinyint unsigned | NO | 0 | OTP 시도 횟수 | | auth_verified_at | datetime | YES | NULL | 인증 완료 일시 | | signature_image_path | varchar(500) | YES | NULL | 서명 이미지 경로 | | signed_at | datetime | YES | NULL | 서명 일시 | | consent_agreed_at | datetime | YES | NULL | 동의 일시 | | sign_ip_address | varchar(45) | YES | NULL | 서명 IP (IPv6 지원) | | sign_user_agent | varchar(500) | YES | NULL | 서명 User Agent | | status | enum | NO | waiting | 서명자 상태 | | rejected_reason | text | YES | NULL | 거절 사유 | | created_at | timestamp | NO | CURRENT | 생성일시 | | updated_at | timestamp | NO | CURRENT | 수정일시 | **인덱스**: `access_token` (UNIQUE), `contract_id` (INDEX) ### 6.3 esign_sign_fields (서명 필드) | 컬럼 | 타입 | NULL | 기본값 | 설명 | |------|------|------|--------|------| | id | bigint unsigned | NO | AUTO | PK | | tenant_id | bigint unsigned | NO | - | 테넌트 ID | | contract_id | bigint unsigned | NO | - | 계약 ID (FK) | | signer_id | bigint unsigned | NO | - | 서명자 ID (FK) | | page_number | int unsigned | NO | - | 페이지 번호 | | position_x | decimal(5,2) | NO | - | X 좌표 (%) | | position_y | decimal(5,2) | NO | - | Y 좌표 (%) | | width | decimal(5,2) | NO | - | 너비 (%) | | height | decimal(5,2) | NO | - | 높이 (%) | | field_type | enum | NO | signature | 필드 유형 | | field_label | varchar(100) | YES | NULL | 필드 라벨 | | field_value | text | YES | NULL | 필드 값 | | is_required | boolean | NO | true | 필수 여부 | | sort_order | int | NO | 0 | 정렬 순서 | | created_at | timestamp | NO | CURRENT | 생성일시 | | updated_at | timestamp | NO | CURRENT | 수정일시 | ### 6.4 esign_audit_logs (감사 로그) | 컬럼 | 타입 | NULL | 기본값 | 설명 | |------|------|------|--------|------| | id | bigint unsigned | NO | AUTO | PK | | tenant_id | bigint unsigned | NO | - | 테넌트 ID | | contract_id | bigint unsigned | NO | - | 계약 ID (FK) | | signer_id | bigint unsigned | YES | NULL | 서명자 ID (FK) | | action | varchar(50) | NO | - | 액션 유형 | | ip_address | varchar(45) | YES | NULL | IP 주소 | | user_agent | varchar(500) | YES | NULL | User Agent | | metadata | json | YES | NULL | 추가 메타데이터 | | created_at | timestamp | NO | CURRENT | 생성일시 | **특징**: `updated_at` 없음, Soft Delete 없음 (삭제 불가) --- ## 7. 에러 코드 ### 7.1 HTTP 상태 코드 | 코드 | 설명 | 사용 상황 | |------|------|----------| | 200 | 성공 | 모든 성공 응답 | | 400 | Bad Request | 비즈니스 로직 에러 | | 401 | Unauthorized | 인증 실패 | | 404 | Not Found | 리소스 없음 | | 422 | Unprocessable Entity | 유효성 검증 실패 | | 500 | Internal Server Error | 서버 오류 | ### 7.2 E-Sign 에러 메시지 | 키 (i18n) | 메시지 | 상태 코드 | |-----------|--------|----------| | `error.esign.invalid_token` | 유효하지 않은 토큰입니다 | 404 | | `error.esign.token_expired` | 토큰이 만료되었습니다 | 400 | | `error.esign.already_completed` | 이미 완료된 계약입니다 | 400 | | `error.esign.already_cancelled` | 이미 취소된 계약입니다 | 400 | | `error.esign.invalid_status_for_send` | 발송 가능한 상태가 아닙니다 | 400 | | `error.esign.no_sign_fields` | 서명 필드가 설정되지 않았습니다 | 400 | | `error.esign.cannot_remind` | 알림을 발송할 수 없습니다 | 400 | | `error.esign.fields_only_in_draft` | 초안 상태에서만 필드를 설정할 수 있습니다 | 400 | | `error.esign.contract_not_signable` | 서명 가능한 계약이 아닙니다 | 400 | | `error.esign.otp_max_attempts` | OTP 최대 시도 횟수를 초과했습니다 | 400 | | `error.esign.otp_not_sent` | OTP가 발송되지 않았습니다 | 400 | | `error.esign.otp_expired` | OTP가 만료되었습니다 | 400 | | `error.esign.otp_invalid` | OTP가 일치하지 않습니다 | 400 | | `error.esign.not_verified` | OTP 인증이 필요합니다 | 400 | | `error.esign.already_signed` | 이미 서명되었습니다 | 400 | | `error.esign.file_not_found` | 파일을 찾을 수 없습니다 | 404 | --- ## 8. 워크플로우 ### 8.1 기본 서명 플로우 ``` [관리자] [시스템] [서명자] │ │ │ │── POST /contracts ──────────────>│ │ │<── 201 계약 생성 (draft) ────────│ │ │ │ │ │── POST /contracts/{id}/fields ──>│ │ │<── 200 필드 설정 완료 ───────────│ │ │ │ │ │── POST /contracts/{id}/send ────>│ │ │<── 200 발송 완료 (pending) ──────│── Email 발송 ──────────────────>│ │ │ │ │ │<── GET /sign/{token} ───────────│ │ │── 200 계약 정보 ───────────────>│ │ │ │ │ │<── POST /sign/{token}/otp/send ─│ │ │── 200 OTP 발송 ────────────────>│ │ │ │ │ │<── POST /sign/{token}/otp/verify│ │ │── 200 인증 완료 + 세션토큰 ────>│ │ │ │ │ │<── GET /sign/{token}/document ──│ │ │── 200 PDF 스트리밍 ────────────>│ │ │ │ │ │<── POST /sign/{token}/submit ───│ │ │── 200 서명 완료 ───────────────>│ │ │ │ │ │ (다음 서명자에게 Email) │ │ │ │ │ │ (모든 서명 완료 → completed) │ ``` ### 8.2 상태 전이 다이어그램 #### 계약 상태 ``` draft ──(send)──> pending ──(1명 서명)──> partially_signed ──(전원 서명)──> completed │ │ │ │ │──(거절)──> rejected │──(거절)──> rejected │ │ │ │──(취소)──> cancelled ──(취소)──> cancelled ──(취소)──> cancelled │ │──(만료)──> expired ``` #### 서명자 상태 ``` waiting ──(알림)──> notified ──(OTP인증)──> authenticated ──(서명)──> signed │ │ │──(거절)──> rejected │──(거절)──> rejected ``` ### 8.3 순차 서명 (counterpart_first) ``` 1. 계약 발송 → 상대방(sign_order=1)에게 이메일 2. 상대방 서명 완료 → 계약 상태: partially_signed 3. 작성자(sign_order=2)에게 자동 이메일 발송 4. 작성자 서명 완료 → 계약 상태: completed ``` ### 8.4 순차 서명 (creator_first) ``` 1. 계약 발송 → 작성자(sign_order=1)에게 이메일 2. 작성자 서명 완료 → 계약 상태: partially_signed 3. 상대방(sign_order=2)에게 자동 이메일 발송 4. 상대방 서명 완료 → 계약 상태: completed ``` --- ## 9. 이메일 알림 ### 9.1 EsignRequestMail 서명 요청 이메일을 발송합니다. | 항목 | 값 | |------|-----| | 발송 주체 | `shine1324@gmail.com` (Gmail SMTP) | | 수신자 | 서명자 이메일 | | 제목 | `[SAM E-Sign] 전자계약 서명을 요청합니다` | | 발송 방식 | `Mail::queue()` (비동기) | **발송 시점**: - 계약 발송 시: 첫 번째 서명자에게 - 이전 서명자 서명 완료 시: 다음 서명자에게 - 리마인더 발송 시: 미서명 서명자에게 **이메일 내용**: - 계약 제목 - 계약 설명 - 만료일 - 서명 링크: `{MNG_URL}/esign/sign/{access_token}` --- ## 10. 보안 ### 10.1 인증 계층 | 계층 | 방식 | 대상 | |------|------|------| | 1단계 | API Key | 모든 API | | 2단계 | Bearer Token (Sanctum) | 계약 관리 API | | 2단계 | Access Token (128자) | 서명 프로세스 API | | 3단계 | OTP (6자리, 5분) | 서명 제출 전 | ### 10.2 데이터 보호 | 항목 | 방법 | |------|------| | 파일 무결성 | SHA-256 해시 생성 및 검증 | | 데이터 격리 | Multi-Tenant (`tenant_id` 글로벌 스코프) | | 감사 추적 | 모든 액션 로그 (IP, User Agent, 시각) | | 토큰 보안 | 128자 랜덤 + 만료일 | | OTP 보안 | 5분 만료 + 최대 5회 시도 | | 전송 보안 | HTTPS (TLS) | ### 10.3 파일 저장 경로 ``` storage/app/esign/{tenant_id}/ ├── originals/ # 원본 PDF │ └── {uuid}.pdf ├── signatures/ # 서명 이미지 │ └── {contract_id}_{signer_id}.png └── signed/ # 서명 완료 PDF └── {uuid}_signed.pdf ``` --- ## 11. 부록: Enum 값 정리 ### 계약 상태 (esign_contracts.status) | 값 | 한글 | 설명 | |----|------|------| | `draft` | 초안 | 생성됨, 아직 발송 전 | | `pending` | 대기 | 서명 요청 발송됨 | | `partially_signed` | 일부 서명 | 1명 이상 서명 완료 | | `completed` | 완료 | 모든 서명 완료 | | `expired` | 만료 | 기한 초과 | | `cancelled` | 취소 | 관리자가 취소 | | `rejected` | 거절 | 서명자가 거절 | ### 서명 순서 (esign_contracts.sign_order_type) | 값 | 한글 | 설명 | |----|------|------| | `counterpart_first` | 상대방 먼저 | 상대방 → 작성자 순서 | | `creator_first` | 작성자 먼저 | 작성자 → 상대방 순서 | ### 서명자 역할 (esign_signers.role) | 값 | 한글 | 설명 | |----|------|------| | `creator` | 작성자 | 계약 작성자 (갑) | | `counterpart` | 상대방 | 서명 상대방 (을) | ### 서명자 상태 (esign_signers.status) | 값 | 한글 | 설명 | |----|------|------| | `waiting` | 대기 | 초기 상태 | | `notified` | 알림됨 | 이메일 발송됨 | | `authenticated` | 인증됨 | OTP 인증 완료 | | `signed` | 서명됨 | 서명 제출 완료 | | `rejected` | 거절됨 | 서명 거절 | ### 필드 유형 (esign_sign_fields.field_type) | 값 | 한글 | 설명 | |----|------|------| | `signature` | 서명 | 수기 서명 | | `stamp` | 도장 | 도장 이미지 | | `text` | 텍스트 | 텍스트 입력 | | `date` | 날짜 | 날짜 입력 | | `checkbox` | 체크박스 | 체크 표시 | ### 감사 로그 액션 (esign_audit_logs.action) | 값 | 한글 | 설명 | |----|------|------| | `created` | 생성 | 계약 생성 | | `sent` | 발송 | 서명 요청 발송 | | `viewed` | 조회 | 서명자가 계약 조회 | | `otp_sent` | OTP 발송 | OTP 이메일 발송 | | `authenticated` | 인증 | OTP 인증 완료 | | `signed` | 서명 | 서명 제출 | | `rejected` | 거절 | 서명 거절 | | `completed` | 완료 | 모든 서명 완료 | | `cancelled` | 취소 | 관리자가 계약 취소 | | `reminded` | 리마인드 | 리마인더 발송 | | `downloaded` | 다운로드 | PDF 파일 다운로드 | --- > **문서 끝** | SAM E-Sign API 명세서 v1.0.0