Files
sam-docs/projects/e-sign/implementation-guide.md
김보곤 aab9dc0799 docs:E-Sign 기술 스택 문서 업데이트 (실제 구현 반영)
- FPDI/FPDF → FPDI/TCPDF (PDF 서명 합성, MNG PdfSignatureService)
- DOCX→PDF 변환 추가 (LibreOffice headless, MNG DocxToPdfConverter)
- GD 확장, 나눔 폰트, Lucide 아이콘 등 실제 사용 기술 반영
- 4개 문서 일괄 업데이트 (technical-design, implementation-guide, operations-guide, changelog)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 13:02:16 +09:00

27 KiB

SAM E-Sign 구현 가이드

프로젝트명: SAM E-Sign (전자계약 서명 솔루션) 구현일: 2026-02-12 버전: v1.0 설계 문서: technical-design.md


1. 구현 개요

모두싸인과 유사한 간편 전자계약 서명 솔루션을 SAM 시스템에 구현했다. API 프로젝트에 백엔드 로직(모델, 서비스, 컨트롤러, 라우트)을, MNG 프로젝트에 프론트엔드(컨트롤러, React 뷰)를 구축했다.

구현 범위

영역 내용
데이터베이스 마이그레이션 4개 (esign_ 접두사 테이블)
모델 4개 (EsignContract, EsignSigner, EsignSignField, EsignAuditLog)
API 서비스 4개 (Contract, Sign, Pdf, Audit)
MNG 서비스 2개 (DocxToPdfConverter, PdfSignatureService)
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개 메서드)

app/Services/ESign/
├── DocxToPdfConverter.php         # DOCX→PDF 변환 (LibreOffice headless)
└── PdfSignatureService.php        # PDF 서명 합성 (FPDI/TCPDF)

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

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

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

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

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 (API)

파일: api/app/Services/ESign/EsignPdfService.php

메서드 설명 상태
generateHash(string $filePath) SHA-256 해시 생성 구현 완료
verifyIntegrity(string $filePath, string $expectedHash) 해시 비교 검증 (hash_equals) 구현 완료
composeSigned(...) 원본 PDF + 서명 이미지 합성 스텁 (MNG로 이관)
addAuditPage(...) 감사 증적 페이지 추가 스텁 (추후)

5.5 DocxToPdfConverter (MNG)

파일: mng/app/Services/ESign/DocxToPdfConverter.php 의존성: LibreOffice (headless), 나눔 폰트

메서드 설명 상태
convertAndStore(UploadedFile $file) DOCX/DOC 파일을 LibreOffice로 PDF 변환 후 저장 구현 완료

동작 방식:

  • Word 파일(.doc, .docx) 업로드 시 자동 감지
  • libreoffice --headless --convert-to pdf 명령으로 변환
  • 나눔 폰트로 한글 정상 렌더링 지원
  • 변환된 PDF를 storage/app/private/esign/{tenant_id}/originals/ 에 저장

5.6 PdfSignatureService (MNG)

파일: mng/app/Services/ESign/PdfSignatureService.php 의존성: FPDI, TCPDF, GD 확장

메서드 설명 상태
mergeSignatures(EsignContract $contract) 원본 PDF에 모든 서명 이미지 오버레이 합성 구현 완료

동작 방식:

  • FPDI로 원본 PDF 임포트
  • 서명 필드를 페이지별로 그룹핑
  • 필드 타입별 렌더링: signature/stamp(이미지), date(텍스트), text(텍스트), checkbox(체크마크)
  • 서명된 PDF를 storage/app/private/esign/{tenant_id}/signed/ 에 저장

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() 패턴을 따른다:

// 성공
{
  "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.phpesign 배열)

메시지
created 전자계약이 생성되었습니다
cancelled 전자계약이 취소되었습니다
sent 서명 요청이 발송되었습니다
reminded 리마인더가 발송되었습니다
fields_configured 서명 필드가 설정되었습니다
otp_sent 인증 코드가 발송되었습니다
otp_verified 본인인증이 완료되었습니다
signed 서명이 완료되었습니다
rejected 서명이 거절되었습니다
completed 전자계약이 완료되었습니다
verified 문서 무결성이 확인되었습니다
downloaded 문서가 다운로드되었습니다

error 키 (lang/ko/error.phpesign 배열)

메시지
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 기반 메뉴
DOCX→PDF app/Services/ESign/DocxToPdfConverter.php LibreOffice headless 변환
PDF 서명 합성 app/Services/ESign/PdfSignatureService.php FPDI/TCPDF 서명 오버레이

12. 추후 구현 예정

항목 우선순위 설명
PDF 합성 (FPDI) 높음 원본 PDF에 서명 이미지 오버레이구현 완료 (MNG PdfSignatureService)
DOCX→PDF 변환 높음 Word 문서 지원구현 완료 (MNG DocxToPdfConverter + LibreOffice)
감사 증적 페이지 높음 완료 PDF 마지막에 감사 정보 페이지 추가
파일 암호화 (AES-256) 중간 원본 PDF 암호화 저장
만료 자동 처리 중간 스케줄러로 expires_at 초과 계약 expired 처리
리마인더 자동 발송 낮음 만료 3일 전 자동 리마인드
SMS OTP 낮음 이메일 외 SMS 인증 지원
OTP bcrypt 해싱 중간 현재 평문 저장 → bcrypt 해싱
PDF 서명 텍스트 한글 낮음 TCPDF CJK 폰트 추가 (현재 helvetica만 사용)

13. 메뉴 추가 안내

메뉴 시더 실행 금지 규칙에 따라, 아래 tinker 명령어를 사용자가 직접 실행해야 합니다:

# 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 구현 결과를 기록한 것입니다. 추후 기능 추가 시 업데이트됩니다.