Files
sam-docs/projects/e-sign/api-specification.md
2026-02-12 08:56:29 +09:00

42 KiB

SAM E-Sign API 명세서

버전: 1.0.0 최종 수정: 2026-02-12 작성자: IT 혁신팀


목차

  1. 개요
  2. 인증
  3. 공통 규칙
  4. 계약 관리 API
  5. 서명 프로세스 API
  6. 데이터 모델
  7. 에러 코드
  8. 워크플로우
  9. 이메일 알림
  10. 보안
  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가 필요합니다.

X-API-Key: {api-key}
  • 미들웨어: auth.apikey
  • API Key가 없거나 유효하지 않으면 401 Unauthorized 반환

2.2 Bearer Token 인증 (계약 관리 API)

계약 관리 API는 추가로 Bearer Token이 필요합니다.

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 응답 형식

성공 응답

{
    "success": true,
    "message": "요청 성공 메시지",
    "data": { ... }
}

에러 응답

{
    "success": false,
    "message": "[에러코드] 에러 메시지",
    "error": {
        "code": 404,
        "details": null
    }
}

유효성 검증 에러 (422)

{
    "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 표준 페이지네이션 형식을 사용합니다.

{
    "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

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)

{
    "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

GET /api/v1/esign/contracts/stats HTTP/1.1
X-API-Key: {api-key}
Authorization: Bearer {token}

Response (200 OK)

{
    "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

GET /api/v1/esign/contracts/1 HTTP/1.1
X-API-Key: {api-key}
Authorization: Bearer {token}

Response (200 OK)

{
    "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)

{
    "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)

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)

{
    "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)

{
    "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

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)

{
    "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

POST /api/v1/esign/contracts/1/send HTTP/1.1
X-API-Key: {api-key}
Authorization: Bearer {token}

Response (200 OK)

{
    "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. 계약 상태: draftpending
  2. 첫 번째 서명자(sign_order=1) 상태: waitingnotified
  3. 첫 번째 서명자에게 EsignRequestMail 발송
  4. 감사 로그 기록 (sent)

4.7 리마인더 발송

POST /api/v1/esign/contracts/{id}/remind

아직 서명하지 않은 서명자에게 알림 이메일을 재발송합니다.

Path Parameters

파라미터 타입 설명
id integer 계약 ID

Response (200 OK)

{
    "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)

{
    "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"

<binary PDF data>

에러 조건:

  • 404: 계약 또는 파일을 찾을 수 없음

4.10 무결성 검증

GET /api/v1/esign/contracts/{id}/verify

저장된 해시와 현재 파일의 해시를 비교하여 문서 변조 여부를 확인합니다.

Path Parameters

파라미터 타입 설명
id integer 계약 ID

Response (200 OK)

{
    "success": true,
    "message": "무결성 검증 완료",
    "data": {
        "verified": true,
        "original_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
    }
}

변조가 감지된 경우:

{
    "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)

{
    "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)

{
    "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

{
    "otp_code": "482637"
}

Response (200 OK)

{
    "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

<binary PDF data>

참고: 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

{
    "signature_image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
}

Response (200 OK)

{
    "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. 서명자 상태: authenticatedsigned
  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

{
    "reason": "계약 조건에 대해 추가 협의가 필요합니다."
}

Response (200 OK)

{
    "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