# SAM E-Sign 운영/배포 가이드

> **프로젝트명**: SAM E-Sign (전자계약 서명 솔루션)
> **작성일**: 2026-02-12
> **버전**: v1.0
> **대상**: 시스템 운영자, DevOps 엔지니어

---

## 목차

1. [시스템 구성](#1-시스템-구성)
2. [환경 변수](#2-환경-변수)
3. [배포 절차](#3-배포-절차)
4. [스토리지 관리](#4-스토리지-관리)
5. [메일 발송 설정](#5-메일-발송-설정)
6. [스케줄러 설정](#6-스케줄러-설정)
7. [모니터링](#7-모니터링)
8. [백업 및 복구](#8-백업-및-복구)
9. [보안 운영](#9-보안-운영)
10. [장애 대응](#10-장애-대응)
11. [확장 가이드](#11-확장-가이드)

---

## 1. 시스템 구성

### 1.1 인프라 아키텍처

```
                    인터넷
                      │
                      ▼
              ┌───────────────┐
              │  Nginx (443)  │  sam-nginx-1
              │  SSL 종단점   │  api.sam.kr / mng.sam.kr
              └──────┬────────┘
                     │
          ┌──────────┴──────────┐
          ▼                     ▼
   ┌──────────────┐    ┌──────────────┐
   │  API (FPM)   │    │  MNG (FPM)   │
   │  sam-api-1   │    │  sam-mng-1   │
   │  :9000       │    │  :9000       │
   └──────┬───────┘    └──────┬───────┘
          │                    │
          └────────┬───────────┘
                   ▼
          ┌──────────────┐     ┌──────────────┐
          │  MySQL 8.0   │     │  phpMyAdmin   │
          │  sam-mysql-1 │     │  :8080        │
          │  :3306       │     └──────────────┘
          └──────────────┘
```

### 1.2 도메인 매핑

| 도메인 | 대상 컨테이너 | 용도 | 프로토콜 |
|--------|-------------|------|---------|
| `api.sam.kr` | sam-api-1:9000 | E-Sign API 백엔드 | HTTPS |
| `mng.sam.kr` | sam-mng-1:9000 | E-Sign 관리 화면 + 공개 서명 | HTTPS |

### 1.3 E-Sign 관련 컨테이너

| 컨테이너 | 역할 | E-Sign 관련 기능 |
|----------|------|-----------------|
| **sam-api-1** | API 서버 | 계약 CRUD, OTP, 서명 처리, 메일 발송, PDF 처리 |
| **sam-mng-1** | 관리 화면 | 대시보드, 계약 생성, 서명 위치 지정, 공개 서명 UI |
| **sam-mysql-1** | 데이터베이스 | esign_* 4개 테이블 |
| **sam-nginx-1** | 웹서버 | SSL, 리버스 프록시, 보안 헤더 |

### 1.4 기술 스택

| 항목 | 버전 | 비고 |
|------|------|------|
| PHP | 8.3 | |
| Laravel | 11 (API), 11 (MNG) | |
| MySQL | 8.0 | Multi-tenant |
| Nginx | 최신 | |
| Docker Compose | v2 | |
| React | 18 (CDN + Babel) | 브라우저 트랜스파일링 |
| FPDI/TCPDF | 2.6 / 6.10 | PDF 서명 합성 (MNG) |
| LibreOffice | headless (writer-nogui) | DOCX→PDF 변환 (MNG) |
| GD 확장 | PHP 내장 | 서명 이미지 처리 (MNG) |
| 나눔 폰트 | fonts-nanum | DOCX 한글 렌더링 (MNG) |
| PDF.js | CDN | 브라우저 PDF 표시 |
| signature_pad.js | 4.x | 터치/마우스 서명 캡처 |
| Lucide | CDN | 아이콘 |

---

## 2. 환경 변수

### 2.1 API 프로젝트 (`/home/aweso/sam/api/.env`)

#### 필수 설정

```bash
# 애플리케이션
APP_URL=https://api.sam.kr/
APP_ENV=production              # local → production
APP_DEBUG=false                 # true → false (운영)
APP_TIMEZONE=Asia/Seoul
APP_LOCALE=ko

# 데이터베이스
DB_CONNECTION=mysql
DB_HOST=sam-mysql-1             # Docker 네트워크
DB_PORT=3306
DB_DATABASE=samdb
DB_USERNAME=samuser
DB_PASSWORD=sampass

# 큐 (메일 비동기 발송)
QUEUE_CONNECTION=database
```

#### E-Sign 관련 설정

```bash
# 메일 (서명 요청/OTP 발송용)
MAIL_MAILER=smtp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=shine1324@gmail.com
MAIL_PASSWORD=<앱 비밀번호>
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=shine1324@gmail.com
MAIL_FROM_NAME="SAM E-Sign"

# 파일 저장
FILESYSTEM_DISK=local
FILE_MAX_SIZE=20480             # 20MB (KB 단위)

# 인증 토큰
SANCTUM_ACCESS_TOKEN_EXPIRATION=120     # 2시간
SANCTUM_REFRESH_TOKEN_EXPIRATION=10080  # 7일
```

#### 환경별 차이

| 설정 | 로컬 (Docker) | 서버 (운영) |
|------|-------------|-------------|
| APP_ENV | local | production |
| APP_DEBUG | true | **false** |
| DB_HOST | sam-mysql-1 | 127.0.0.1 |
| MAIL_MAILER | log (테스트) | smtp (실제 발송) |
| QUEUE_CONNECTION | sync | database |

### 2.2 MNG 프로젝트 (`/home/aweso/sam/mng/.env`)

```bash
# 애플리케이션
APP_URL=https://mng.sam.kr
APP_ENV=production
APP_DEBUG=false

# 데이터베이스 (API와 동일 DB 공유)
DB_HOST=sam-mysql-1
DB_DATABASE=samdb
DB_USERNAME=samuser
DB_PASSWORD=sampass
```

> MNG는 화면 렌더링만 담당하므로 E-Sign 관련 별도 환경변수는 없습니다.
> API 호출은 프론트엔드(React)에서 `api.sam.kr`로 직접 요청합니다.

---

## 3. 배포 절차

### 3.1 최초 배포 (신규 설치)

#### Step 1: 마이그레이션 실행

```bash
# 로컬 (Docker)
docker exec sam-api-1 php artisan migrate

# 서버
cd /home/webservice/api
php artisan migrate
```

마이그레이션 결과 확인:

```bash
# 4개 테이블 생성 확인
docker exec sam-api-1 php artisan migrate:status | grep esign

# 기대 결과:
# Ran  2026_02_12_100000_create_esign_contracts_table
# Ran  2026_02_12_110000_create_esign_signers_table
# Ran  2026_02_12_120000_create_esign_sign_fields_table
# Ran  2026_02_12_130000_create_esign_audit_logs_table
```

#### Step 2: 스토리지 디렉토리 생성

```bash
# 로컬 (Docker)
docker exec sam-api-1 mkdir -p storage/app/esign

# 서버
cd /home/webservice/api
mkdir -p storage/app/esign
chmod 775 storage/app/esign
chown -R www-data:www-data storage/app/esign
```

#### Step 3: 라우트 캐시 갱신

```bash
# 로컬
docker exec sam-api-1 php artisan route:cache
docker exec sam-mng-1 php artisan route:cache

# 서버
cd /home/webservice/api && php artisan route:cache
cd /home/webservice/mng && php artisan route:cache
```

#### Step 4: 메뉴 등록

메뉴 시더 실행 금지. tinker로 수동 등록합니다.

```bash
# 상위 메뉴 생성
ssh sam-server "cd /home/webservice/mng && php artisan tinker --execute=\"
\\\$parent = App\\\\Models\\\\Commons\\\\Menu::create([
    'tenant_id' => 1,
    'parent_id' => null,
    'name' => '전자계약 (E-Sign)',
    'url' => '/esign',
    'icon' => 'ti ti-file-certificate',
    'sort_order' => 90,
    'is_active' => true,
]);
echo 'Parent ID: ' . \\\$parent->id;
\""

# 하위 메뉴 생성 (parent_id를 위에서 출력된 값으로 교체)
ssh sam-server "cd /home/webservice/mng && php artisan tinker --execute=\"
App\\\\Models\\\\Commons\\\\Menu::create([
    'tenant_id' => 1,
    'parent_id' => <PARENT_ID>,
    'name' => '계약 대시보드',
    'url' => '/esign',
    'icon' => 'ti ti-dashboard',
    'sort_order' => 1,
    'is_active' => true,
]);
App\\\\Models\\\\Commons\\\\Menu::create([
    'tenant_id' => 1,
    'parent_id' => <PARENT_ID>,
    'name' => '새 계약 생성',
    'url' => '/esign/create',
    'icon' => 'ti ti-file-plus',
    'sort_order' => 2,
    'is_active' => true,
]);
\""
```

#### Step 5: 라우트 확인

```bash
# API 라우트 확인 (16개)
docker exec sam-api-1 php artisan route:list --path=esign

# MNG 라우트 확인 (8개)
docker exec sam-mng-1 php artisan route:list --path=esign
```

### 3.2 업데이트 배포

코드 변경 사항을 반영할 때:

#### 로컬 (Docker)

```bash
# 1. 코드 pull
cd /home/aweso/sam/api && git pull
cd /home/aweso/sam/mng && git pull

# 2. 의존성 업데이트 (composer.json 변경 시)
docker exec sam-api-1 composer install --no-dev --optimize-autoloader
docker exec sam-mng-1 composer install --no-dev --optimize-autoloader

# 3. 마이그레이션 (API에서만)
docker exec sam-api-1 php artisan migrate

# 4. 캐시 갱신
docker exec sam-api-1 php artisan config:cache
docker exec sam-api-1 php artisan route:cache
docker exec sam-api-1 php artisan view:cache
docker exec sam-mng-1 php artisan config:cache
docker exec sam-mng-1 php artisan route:cache
docker exec sam-mng-1 php artisan view:cache
```

#### 서버 (운영)

```bash
# API 프로젝트
cd /home/webservice/api
git pull
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache

# MNG 프로젝트 (마이그레이션 없음)
cd /home/webservice/mng
git pull
composer install --no-dev --optimize-autoloader
php artisan config:cache
php artisan route:cache
php artisan view:cache
```

### 3.3 배포 체크리스트

```
□ API: git pull 완료
□ MNG: git pull 완료
□ API: composer install 완료
□ MNG: composer install 완료
□ API: php artisan migrate 완료 (신규 마이그레이션 시)
□ API: php artisan config:cache
□ MNG: php artisan config:cache
□ API: route:list --path=esign 로 16개 라우트 확인
□ MNG: route:list --path=esign 로 8개 라우트 확인
□ 브라우저: mng.sam.kr/esign 접속 확인
□ API: esign 테이블 4개 존재 확인
□ 메일: 테스트 서명 요청 발송 → 이메일 수신 확인
```

---

## 4. 스토리지 관리

### 4.1 파일 저장 구조

```
storage/app/esign/
└── {tenant_id}/
    ├── originals/              # 원본 PDF
    │   └── {contract_id}/
    │       └── {hash}.pdf
    ├── signatures/             # 서명 이미지
    │   └── {signer_id}/
    │       └── {timestamp}.png
    └── signed/                 # 서명 완료 PDF (v1.1)
        └── {contract_id}/
            └── {contract_code}_signed.pdf
```

### 4.2 용량 산정

| 파일 유형 | 평균 크기 | 월 100건 기준 |
|----------|----------|--------------|
| 원본 PDF | 2MB | 200MB |
| 서명 이미지 | 50KB x 2인 | 10MB |
| 서명 완료 PDF | 2.5MB (v1.1) | 250MB |
| **월 합계** | | **약 460MB** |
| **연간 합계** | | **약 5.5GB** |

### 4.3 스토리지 모니터링

```bash
# 전체 E-Sign 스토리지 용량 확인
du -sh /home/webservice/api/storage/app/esign/

# 테넌트별 용량 확인
du -sh /home/webservice/api/storage/app/esign/*/

# 디스크 여유 공간 확인
df -h /home/webservice/
```

### 4.4 파일 정리 정책

| 항목 | 보관 기간 | 근거 |
|------|----------|------|
| 완료된 계약 PDF | 5년 | 전자상거래법 |
| 서명 이미지 | 5년 | 법적 증거 |
| 취소/만료 계약 PDF | 1년 | 운영 판단 |
| 감사 로그 | 영구 | 삭제 불가 |

### 4.5 권한 설정

```bash
# API 스토리지 디렉토리 권한
chown -R www-data:www-data /home/webservice/api/storage/app/esign/
chmod -R 775 /home/webservice/api/storage/app/esign/

# 보안: 웹에서 직접 접근 차단 (Nginx에서 처리)
# storage 디렉토리는 public 경로에 포함되지 않음
```

---

## 5. 메일 발송 설정

### 5.1 발송 시점

| 이벤트 | 수신자 | 메일 내용 |
|--------|--------|----------|
| 서명 요청 발송 | 첫 번째 서명자 | 계약 제목, 서명 링크, 기한 |
| OTP 발송 | 해당 서명자 | 6자리 인증코드, 5분 유효 |
| 리마인더 | 미서명 서명자 | 서명 독촉, 남은 기한 |
| 서명 완료 알림 | 다음 서명자 | "상대방이 서명했습니다", 서명 링크 |
| 계약 완료 | 양쪽 서명자 | 완료 안내, 다운로드 링크 |

### 5.2 SMTP 설정

```bash
# API .env (현재 Gmail SMTP)
MAIL_MAILER=smtp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=shine1324@gmail.com
MAIL_PASSWORD=<Gmail 앱 비밀번호>
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=shine1324@gmail.com
MAIL_FROM_NAME="SAM E-Sign"
```

### 5.3 Gmail 앱 비밀번호 발급

1. Google 계정 → 보안 → 2단계 인증 활성화
2. 앱 비밀번호 생성 (항목: 메일, 기기: 기타 → "SAM API")
3. 생성된 16자리 비밀번호를 `.env`의 `MAIL_PASSWORD`에 설정

### 5.4 메일 발송 테스트

```bash
# tinker를 이용한 메일 발송 테스트
docker exec sam-api-1 php artisan tinker --execute="
Mail::raw('E-Sign 메일 테스트', function(\$m) {
    \$m->to('test@example.com')->subject('SAM E-Sign 테스트');
});
echo 'Mail sent';
"
```

### 5.5 메일 발송 실패 시

```bash
# 메일 로그 확인
docker exec sam-api-1 tail -50 storage/logs/laravel.log | grep -i mail

# 큐 실패 작업 확인
docker exec sam-api-1 php artisan queue:failed

# 실패 작업 재시도
docker exec sam-api-1 php artisan queue:retry all
```

### 5.6 운영 환경 권장 사항

| 항목 | 현재 | 운영 권장 |
|------|------|----------|
| SMTP 서비스 | Gmail | AWS SES / Mailgun / SendGrid |
| 일 발송 한도 | 500건 (Gmail) | 무제한 (전문 서비스) |
| 발송 모드 | 동기(sync) | **비동기(database/redis)** |
| 큐 워커 | 미실행 | `php artisan queue:work` 상시 실행 |

---

## 6. 스케줄러 설정

### 6.1 E-Sign 관련 예정 스케줄 작업

> 현재 v1.0에서는 스케줄러 미구현. v1.1에서 추가 예정.

| 작업 | 실행 주기 | 설명 | 상태 |
|------|----------|------|------|
| 계약 만료 처리 | 매일 01:00 | expires_at 초과 계약 → expired 상태 변경 | v1.1 예정 |
| 자동 리마인더 | 매일 09:00 | 만료 3일 전 서명자에게 알림 | v1.1 예정 |
| 임시 파일 정리 | 매일 03:30 | 7일 이상 된 임시 파일 삭제 | 기존 설정 |

### 6.2 기존 SAM 스케줄러 (참고)

API 프로젝트의 `routes/console.php`에 이미 등록된 작업:

```
02:00  일간 통계 집계
03:10  감사 로그 정리 (13개월 보관)
03:30  임시 파일 정리 (7일 이상)
03:40  휴지통 파일 삭제 (30일 이상)
03:50  공유 링크 정리
05:00  DB 백업 상태 확인
```

### 6.3 크론탭 설정

```bash
# 서버 크론탭 (이미 설정된 경우 확인만)
crontab -l

# Laravel 스케줄러 크론 (1분마다 실행)
* * * * * cd /home/webservice/api && php artisan schedule:run >> /dev/null 2>&1
```

---

## 7. 모니터링

### 7.1 헬스체크

```bash
# API 서버 동작 확인
curl -s https://api.sam.kr/api/health | jq .

# MNG 서버 동작 확인
curl -s -o /dev/null -w "%{http_code}" https://mng.sam.kr/esign

# MySQL 연결 확인
docker exec sam-api-1 php artisan tinker --execute="DB::connection()->getPdo(); echo 'DB OK';"

# E-Sign 테이블 확인
docker exec sam-api-1 php artisan tinker --execute="
echo 'contracts: ' . \App\Models\ESign\EsignContract::withoutGlobalScopes()->count();
echo 'signers: ' . \App\Models\ESign\EsignSigner::withoutGlobalScopes()->count();
"
```

### 7.2 로그 모니터링

```bash
# API 에러 로그 실시간 감시
docker exec sam-api-1 tail -f storage/logs/laravel.log | grep -i "error\|exception"

# MNG 에러 로그 실시간 감시
docker exec sam-mng-1 tail -f storage/logs/laravel.log | grep -i "error\|exception"

# E-Sign 관련 로그만 필터
docker exec sam-api-1 grep -i "esign\|e-sign" storage/logs/laravel.log | tail -50
```

### 7.3 주요 모니터링 지표

| 지표 | 확인 방법 | 임계치 |
|------|----------|--------|
| API 응답 시간 | Nginx access log | > 3초 경고 |
| 에러 발생률 | Laravel log | 시간당 10건 이상 경고 |
| 디스크 사용량 | `df -h` | 80% 이상 경고 |
| 메일 발송 실패 | `queue:failed` | 1건 이상 즉시 확인 |
| DB 연결 수 | `SHOW STATUS LIKE 'Threads_connected'` | 80% 이상 경고 |

### 7.4 계약 현황 모니터링

```bash
# 상태별 계약 통계
docker exec sam-api-1 php artisan tinker --execute="
\$stats = \App\Models\ESign\EsignContract::withoutGlobalScopes()
    ->selectRaw('status, count(*) as cnt')
    ->groupBy('status')
    ->pluck('cnt', 'status');
print_r(\$stats->toArray());
"

# 만료 임박 계약 (3일 이내)
docker exec sam-api-1 php artisan tinker --execute="
\$expiring = \App\Models\ESign\EsignContract::withoutGlobalScopes()
    ->whereIn('status', ['pending', 'partially_signed'])
    ->where('expires_at', '<=', now()->addDays(3))
    ->count();
echo '만료 임박: ' . \$expiring . '건';
"
```

---

## 8. 백업 및 복구

### 8.1 백업 대상

| 대상 | 방법 | 주기 |
|------|------|------|
| DB (esign_* 4개 테이블) | mysqldump | 일간 |
| 원본 PDF 파일 | rsync / 파일 복사 | 일간 |
| 서명 이미지 | rsync / 파일 복사 | 일간 |
| .env 설정 파일 | 수동 백업 | 변경 시 |

### 8.2 DB 백업

```bash
# E-Sign 테이블만 백업
docker exec sam-mysql-1 mysqldump -u samuser -psampass samdb \
  esign_contracts esign_signers esign_sign_fields esign_audit_logs \
  > /home/aweso/sam/db_backup/esign_$(date +%Y%m%d).sql

# 전체 DB 백업 (기존 방식 활용)
docker exec sam-mysql-1 mysqldump -u samuser -psampass samdb \
  > /home/aweso/sam/db_backup/samdb_$(date +%Y%m%d).sql
```

### 8.3 파일 백업

```bash
# E-Sign 스토리지 백업
rsync -av /home/webservice/api/storage/app/esign/ \
  /backup/esign/$(date +%Y%m%d)/
```

### 8.4 복구 절차

```bash
# 1. DB 복구
docker exec -i sam-mysql-1 mysql -u samuser -psampass samdb \
  < /home/aweso/sam/db_backup/esign_20260212.sql

# 2. 파일 복구
rsync -av /backup/esign/20260212/ \
  /home/webservice/api/storage/app/esign/

# 3. 권한 복원
chown -R www-data:www-data /home/webservice/api/storage/app/esign/

# 4. 캐시 클리어
cd /home/webservice/api && php artisan cache:clear
```

---

## 9. 보안 운영

### 9.1 Nginx 보안 설정 (적용 완료)

```
# SSL/TLS
- TLSv1.2, TLSv1.3만 허용
- HSTS: max-age=31536000 (1년)

# 보안 헤더
- X-Frame-Options: SAMEORIGIN
- X-Content-Type-Options: nosniff
- X-XSS-Protection: 1; mode=block

# 경로 차단
- ../, .env, .git, .htaccess, .sql 패턴 차단 (403)
- sqlmap, nikto, nmap 등 스캐너 User-Agent 차단 (403)
```

### 9.2 E-Sign 보안 포인트

| 항목 | 보호 방법 | 점검 주기 |
|------|----------|----------|
| access_token | 128자 랜덤, API 응답에 미포함 ($hidden) | 코드 리뷰 시 |
| OTP 코드 | 6자리, 5분 만료, 5회 제한 | 배포 시 |
| 파일 접근 | Controller 스트리밍만 허용, 직접 경로 차단 | 배포 시 |
| 문서 무결성 | SHA-256 hash_equals() (타이밍 공격 방지) | 코드 리뷰 시 |
| 테넌트 격리 | BelongsToTenant 글로벌 스코프 | 배포 시 |
| 감사 로그 | SoftDeletes 미적용, 삭제 API 없음 | 분기 점검 |

### 9.3 정기 보안 점검

```bash
# 1. 비정상 접근 시도 확인
docker exec sam-nginx-1 grep "403\|401" /var/log/nginx/access.log | tail -20

# 2. OTP 시도 횟수 초과 확인
docker exec sam-api-1 php artisan tinker --execute="
\$blocked = \App\Models\ESign\EsignSigner::withoutGlobalScopes()
    ->where('otp_attempts', '>=', 5)
    ->count();
echo 'OTP 차단 서명자: ' . \$blocked . '명';
"

# 3. 만료된 토큰으로 접근 시도 감사
docker exec sam-api-1 php artisan tinker --execute="
\$suspicious = \App\Models\ESign\EsignAuditLog::withoutGlobalScopes()
    ->where('action', 'like', '%failed%')
    ->where('created_at', '>=', now()->subDays(7))
    ->count();
echo '최근 7일 실패 이벤트: ' . \$suspicious . '건';
"
```

### 9.4 SSL 인증서 관리

```bash
# 인증서 만료일 확인
openssl x509 -enddate -noout -in /etc/nginx/ssl/sam.kr.crt

# 인증서 갱신 후 Nginx 재시작
docker exec sam-nginx-1 nginx -s reload
```

---

## 10. 장애 대응

### 10.1 장애 유형별 대응

#### API 서버 응답 없음

```bash
# 1. 컨테이너 상태 확인
docker ps | grep sam-api

# 2. PHP-FPM 프로세스 확인
docker exec sam-api-1 ps aux | grep php-fpm

# 3. 에러 로그 확인
docker exec sam-api-1 tail -50 storage/logs/laravel.log

# 4. 컨테이너 재시작
docker restart sam-api-1
```

#### DB 연결 실패

```bash
# 1. MySQL 컨테이너 확인
docker ps | grep sam-mysql

# 2. MySQL 프로세스 확인
docker exec sam-mysql-1 mysqladmin -u samuser -psampass ping

# 3. 연결 수 확인
docker exec sam-mysql-1 mysql -u samuser -psampass -e \
  "SHOW STATUS LIKE 'Threads_connected';"

# 4. MySQL 재시작
docker restart sam-mysql-1
```

#### 메일 발송 실패

```bash
# 1. SMTP 연결 테스트
docker exec sam-api-1 php artisan tinker --execute="
try {
    \$transport = new \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport(
        'smtp.gmail.com', 587, true
    );
    echo 'SMTP 연결 가능';
} catch (\Exception \$e) {
    echo 'SMTP 에러: ' . \$e->getMessage();
}
"

# 2. 큐 실패 작업 확인
docker exec sam-api-1 php artisan queue:failed

# 3. 실패 작업 재시도
docker exec sam-api-1 php artisan queue:retry all

# 4. 로그에서 메일 에러 확인
docker exec sam-api-1 grep "Swift_Transport\|Mail\|smtp" storage/logs/laravel.log | tail -20
```

#### 서명 링크 접속 불가

```bash
# 1. 토큰 유효성 확인
docker exec sam-api-1 php artisan tinker --execute="
\$signer = \App\Models\ESign\EsignSigner::withoutGlobalScopes()
    ->where('access_token', '<토큰값>')
    ->first();
if (\$signer) {
    echo 'Status: ' . \$signer->status;
    echo ', Expires: ' . \$signer->token_expires_at;
    echo ', Contract: ' . \$signer->contract->status;
} else {
    echo 'Token not found';
}
"

# 2. Nginx 로그에서 해당 요청 확인
docker exec sam-nginx-1 grep "esign/sign" /var/log/nginx/access.log | tail -10
```

#### PDF 업로드 실패

```bash
# 1. 스토리지 디렉토리 권한 확인
docker exec sam-api-1 ls -la storage/app/esign/

# 2. 디스크 여유 공간 확인
docker exec sam-api-1 df -h /var/www/api/storage/

# 3. PHP 업로드 설정 확인
docker exec sam-api-1 php -i | grep -i "upload_max_filesize\|post_max_size"

# 4. Nginx 업로드 크기 확인
docker exec sam-nginx-1 grep "client_max_body_size" /etc/nginx/nginx.conf
```

### 10.2 긴급 복구 체크리스트

```
□ 장애 내용 파악 (에러 메시지, 발생 시점, 영향 범위)
□ 에러 로그 확인 (API, MNG, Nginx, MySQL)
□ 컨테이너 상태 확인 (docker ps)
□ 네트워크 확인 (도메인, DNS, SSL)
□ 서비스 재시작 (필요 시)
□ DB 연결 확인
□ 캐시 클리어 (config:cache, route:cache)
□ 복구 확인 (브라우저 접속 테스트)
□ 장애 보고서 작성 (원인, 조치, 재발 방지)
```

---

## 11. 확장 가이드

### 11.1 현재 설치된 패키지 (구현 완료)

```bash
# FPDI/TCPDF (PDF 서명 합성) - MNG 프로젝트에 설치됨
# setasign/fpdi: ^2.6, tecnickcom/tcpdf: ^6.10

# LibreOffice (DOCX→PDF 변환) - MNG Docker 컨테이너에 설치됨
# libreoffice-writer-nogui, fonts-nanum, fonts-nanum-extra

# GD 확장 (서명 이미지 처리) - MNG Docker 컨테이너에 설치됨
```

### 11.2 큐 워커 설정 (운영 권장)

메일 발송을 비동기로 처리하려면 큐 워커를 상시 실행해야 합니다.

```bash
# Supervisor 설정 (/etc/supervisor/conf.d/esign-worker.conf)
[program:esign-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /home/webservice/api/artisan queue:work database --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasleepy=false
numprocs=2
user=www-data
redirect_stderr=true
stdout_logfile=/home/webservice/api/storage/logs/worker.log
stopwaitsecs=3600

# Supervisor 적용
supervisorctl reread
supervisorctl update
supervisorctl start esign-worker:*
```

### 11.3 S3 스토리지 전환 (선택)

로컬 스토리지에서 AWS S3로 전환할 경우:

```bash
# 패키지 설치
composer require league/flysystem-aws-s3-v3 "^3.0"

# .env 설정 추가
AWS_ACCESS_KEY_ID=<키>
AWS_SECRET_ACCESS_KEY=<시크릿>
AWS_DEFAULT_REGION=ap-northeast-2
AWS_BUCKET=sam-esign
FILESYSTEM_DISK=s3
```

### 11.4 운영 환경 SMTP 전환 (권장)

Gmail SMTP는 일 500건 제한이 있으므로, 운영 환경에서는 전문 메일 서비스를 권장합니다.

**AWS SES 예시:**

```bash
# .env 설정
MAIL_MAILER=ses
AWS_ACCESS_KEY_ID=<키>
AWS_SECRET_ACCESS_KEY=<시크릿>
AWS_DEFAULT_REGION=ap-northeast-2
MAIL_FROM_ADDRESS=esign@sam.kr
```

**SendGrid 예시:**

```bash
# .env 설정
MAIL_MAILER=smtp
MAIL_HOST=smtp.sendgrid.net
MAIL_PORT=587
MAIL_USERNAME=apikey
MAIL_PASSWORD=<SendGrid API Key>
MAIL_FROM_ADDRESS=esign@sam.kr
```

---

## 부록: 명령어 모음

### 일상 운영

```bash
# 서비스 상태 확인
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep sam

# 전체 재시작
cd /home/aweso/sam && docker compose restart

# 개별 재시작
docker restart sam-api-1
docker restart sam-mng-1

# 캐시 전체 클리어
docker exec sam-api-1 php artisan optimize:clear
docker exec sam-mng-1 php artisan optimize:clear
```

### 데이터 조회

```bash
# 계약 건수
docker exec sam-mysql-1 mysql -u samuser -psampass samdb \
  -e "SELECT status, COUNT(*) AS cnt FROM esign_contracts GROUP BY status;"

# 최근 감사 로그
docker exec sam-mysql-1 mysql -u samuser -psampass samdb \
  -e "SELECT action, ip_address, created_at FROM esign_audit_logs ORDER BY id DESC LIMIT 20;"

# 서명 대기 건수
docker exec sam-mysql-1 mysql -u samuser -psampass samdb \
  -e "SELECT COUNT(*) AS pending FROM esign_contracts WHERE status IN ('pending','partially_signed');"
```

### 트러블슈팅

```bash
# Laravel 로그 실시간
docker exec sam-api-1 tail -f storage/logs/laravel.log

# Nginx 접근 로그
docker exec sam-nginx-1 tail -f /var/log/nginx/access.log

# PHP 설정 확인
docker exec sam-api-1 php -i | grep -E "memory_limit|upload_max|post_max|max_execution"

# 라우트 목록
docker exec sam-api-1 php artisan route:list --path=esign --columns=method,uri,name
```

---

*이 문서는 SAM E-Sign v1.0 운영/배포 가이드입니다. 최종 업데이트: 2026-02-12*
