diff --git a/claudedocs/reference/data-sync-local-to-server.md b/claudedocs/reference/data-sync-local-to-server.md new file mode 100644 index 00000000..b6266fc9 --- /dev/null +++ b/claudedocs/reference/data-sync-local-to-server.md @@ -0,0 +1,164 @@ +# 사례: 로컬 → 서버 데이터 동기화 + +> 2026-02-20 실제 수행한 E-Sign 템플릿 마이그레이션 기록 + +## 상황 + +로컬(Docker sam-mng-1)에서 만든 E-Sign 템플릿 3개를 서버(mng.codebridge-x.com)에 옮겨야 했다. +서버에는 템플릿이 0개, 로컬에는 3개 + PDF 파일 3개 + 필드 아이템 56개가 있었다. + +## 환경 제약 + +| 제약 | 이유 | +|------|------| +| 서버 storage에 SCP 불가 | `esign/` 디렉토리가 `www-data:0700` → SSH 사용자(pro) 접근 불가 | +| 서버에서 tinker로 파일 쓰기 불가 | tinker는 pro 사용자로 실행, www-data 디렉토리에 쓰기 권한 없음 | +| 시더 사용 금지 | CLAUDE.md 규칙 | + +## 실행 절차 + +### Step 1. 로컬 데이터 추출 + +```bash +# 로컬 Docker에서 템플릿 데이터 확인 +docker exec sam-mng-1 php artisan tinker --execute=" +use App\Models\ESign\EsignFieldTemplate; +\$templates = EsignFieldTemplate::where('is_active', true)->get(); +foreach (\$templates as \$t) { + echo 'ID: ' . \$t->id . ' | ' . \$t->name . ' | File: ' . \$t->file_path . PHP_EOL; +} +" +``` + +**결과:** +``` +ID: 4 | 비밀유지서약서 | esign/1/templates/q9gGUPT4pCRUhsKXV9GwbymaqIwsM8bYf6QSYQT0.pdf +ID: 5 | 영업파트너 계약서 | esign/1/templates/20260214171217_copy.pdf +ID: 6 | 고객 서비스이용 계약서 | esign/1/templates/Zf5zGDl9Cj6l76DakqPyyu2JpDGIa7mwWBHSuVXm.pdf +``` + +### Step 2. PDF 파일을 Docker → WSL 호스트로 꺼내기 + +```bash +mkdir -p /tmp/esign-templates + +docker cp sam-mng-1:/var/www/mng/storage/app/private/esign/1/templates/q9gGUPT4pCRUhsKXV9GwbymaqIwsM8bYf6QSYQT0.pdf /tmp/esign-templates/ +docker cp sam-mng-1:/var/www/mng/storage/app/private/esign/1/templates/20260214171217_copy.pdf /tmp/esign-templates/ +docker cp sam-mng-1:/var/www/mng/storage/app/private/esign/1/templates/Zf5zGDl9Cj6l76DakqPyyu2JpDGIa7mwWBHSuVXm.pdf /tmp/esign-templates/ +``` + +### Step 3. WSL → 서버 /tmp에 base64로 전송 + +```bash +# SCP로 storage에 직접 넣을 수 없으므로, base64로 /tmp에 전송 +base64 /tmp/esign-templates/q9gGUPT4pCRUhsKXV9GwbymaqIwsM8bYf6QSYQT0.pdf | ssh pro@114.203.209.83 "cat > /tmp/template_nda.b64" +base64 /tmp/esign-templates/20260214171217_copy.pdf | ssh pro@114.203.209.83 "cat > /tmp/template_partner.b64" +base64 /tmp/esign-templates/Zf5zGDl9Cj6l76DakqPyyu2JpDGIa7mwWBHSuVXm.pdf | ssh pro@114.203.209.83 "cat > /tmp/template_customer.b64" +``` + +### Step 4. 서버에 임시 PHP 스크립트 작성 → curl로 실행 + +**핵심: `public/`에 PHP를 놓으면 nginx가 php-fpm(www-data)으로 실행하므로 storage 접근 가능** + +#### 4-1. PDF 저장 스크립트 (`migrate_templates.php`) + +```php +make(Illuminate\Contracts\Http\Kernel::class); +$kernel->handle($request = Illuminate\Http\Request::capture()); + +use Illuminate\Support\Facades\Storage; +header('Content-Type: text/plain; charset=utf-8'); + +$files = [ + 'q9gGUPT4pCRUhsKXV9GwbymaqIwsM8bYf6QSYQT0.pdf' => '/tmp/template_nda.b64', + '20260214171217_copy.pdf' => '/tmp/template_partner.b64', + 'Zf5zGDl9Cj6l76DakqPyyu2JpDGIa7mwWBHSuVXm.pdf' => '/tmp/template_customer.b64', +]; + +foreach ($files as $name => $b64path) { + $data = base64_decode(file_get_contents($b64path)); + Storage::disk('local')->put('esign/1/templates/' . $name, $data); + echo "Saved: $name (" . strlen($data) . " bytes)\n"; +} +``` + +```bash +# 실행 +curl -sk "https://mng.codebridge-x.com/migrate_templates.php" +``` + +**결과:** +``` +Saved: q9gGUPT4pCRUhsKXV9GwbymaqIwsM8bYf6QSYQT0.pdf (215836 bytes) +Saved: 20260214171217_copy.pdf (286772 bytes) +Saved: Zf5zGDl9Cj6l76DakqPyyu2JpDGIa7mwWBHSuVXm.pdf (500493 bytes) +``` + +#### 4-2. DB INSERT 스크립트 (`migrate_templates_db.php`) + +로컬에서 `/tmp/migrate_templates_db.php`로 작성 → SCP로 서버 `public/`에 업로드 → curl 실행 + +```bash +# 로컬에서 PHP 파일 작성 후 SCP 업로드 (public/ 디렉토리는 pro 사용자 쓰기 가능) +scp /tmp/migrate_templates_db.php pro@114.203.209.83:/home/webservice/mng/public/ + +# 실행 +curl -sk "https://mng.codebridge-x.com/migrate_templates_db.php" +``` + +**DB INSERT 스크립트 구조:** +```php + Items: 26 +Created: 영업파트너 계약서 (ID: 2) -> Items: 13 +Created: 고객 서비스이용 계약서 (ID: 3) -> Items: 17 +=== All 3 templates migrated successfully! === +``` + +### Step 5. 임시 파일 정리 + +```bash +# 서버 +ssh pro@114.203.209.83 "rm -f /home/webservice/mng/public/migrate_templates.php \ + /home/webservice/mng/public/migrate_templates_db.php \ + /tmp/template_nda.b64 /tmp/template_partner.b64 /tmp/template_customer.b64" + +# 로컬 +rm -rf /tmp/esign-templates /tmp/migrate_templates_db.php +``` + +## 전체 흐름 요약 + +``` +[로컬 Docker] [WSL 호스트] [서버] + +1. tinker로 데이터 확인 +2. docker cp로 PDF 꺼냄 ──→ /tmp/에 PDF 파일들 + 3. base64 인코딩 + SSH ──→ /tmp/*.b64 도착 + 4. PHP 스크립트 SCP ──→ public/*.php 배치 + 5. curl 실행 ──→ www-data로 실행 + ├─ base64 → Storage::put (PDF) + └─ DB::beginTransaction + ├─ Template::create + └─ TemplateItem::create + 6. 임시 파일 삭제 ──→ public/*.php, /tmp/*.b64 삭제 +``` + +## 주의사항 + +- heredoc(`<< 'EOF'`)으로 서버에 PHP를 직접 쓰면 `$` 이스케이프 문제 발생 → **로컬에서 파일 작성 후 SCP 업로드** 권장 +- `public/`에 놓은 PHP는 **누구나 접근 가능** → 작업 즉시 삭제 필수 +- 서버 DB의 auto_increment ID는 로컬과 다를 수 있음 → FK는 새로 생성된 ID 기준으로 연결 +- 트랜잭션으로 감싸서 중간 실패 시 롤백 보장 diff --git a/claudedocs/reference/data-sync-server-to-local.md b/claudedocs/reference/data-sync-server-to-local.md new file mode 100644 index 00000000..53d7be8b --- /dev/null +++ b/claudedocs/reference/data-sync-server-to-local.md @@ -0,0 +1,173 @@ +# 사례: 서버 → 로컬 데이터 동기화 + +> 2026-02-20 실제 수행한 E-Sign 템플릿 역동기화 기록 + +## 상황 + +서버(mng.codebridge-x.com)에서 새로 만든 "영업파트너 계약서(단체용)" 템플릿 1개를 +로컬(Docker sam-mng-1)에 가져와야 했다. + +## 환경 제약 + +| 제약 | 이유 | +|------|------| +| 서버 storage에서 SCP 다운로드 불가 | `esign/` 디렉토리가 `www-data:0700` → SSH(pro) 읽기 불가 | +| 서버 tinker는 pro 사용자 | storage 파일 읽기 불가, DB 조회만 가능 | + +## 실행 절차 + +### Step 1. 서버 데이터 추출 (DB) + +```bash +ssh pro@114.203.209.83 "cd /home/webservice/mng && php artisan tinker --execute=\" +use App\Models\ESign\EsignFieldTemplate; +use App\Models\ESign\EsignFieldTemplateItem; + +\\\$t = EsignFieldTemplate::where('name', 'like', '%단체%')->first(); +echo 'Name: ' . \\\$t->name . PHP_EOL; +echo 'File Path: ' . \\\$t->file_path . PHP_EOL; +echo 'Variables: ' . json_encode(\\\$t->variables, JSON_UNESCAPED_UNICODE) . PHP_EOL; +# ... 필드 아이템도 JSON으로 추출 +\"" +``` + +**결과:** +``` +Name: 영업파트너 계약서(단체용) +File Path: esign/1/templates/7Vi374wZwcRg5bjlXPJiASkizxLSJlCNDJhaMBeo.pdf +Variables: [{"key":"partnername","type":"text","label":"파트너명"}, ...] +Items: 13개 (JSON) +``` + +**포인트:** tinker는 `pro` 사용자지만 DB 읽기는 가능. 파일 읽기만 불가. + +### Step 2. 서버 PDF 파일 추출 (임시 PHP → base64) + +```bash +# 서버 public/에 임시 PHP 작성 +ssh pro@114.203.209.83 "cat > /home/webservice/mng/public/_export_pdf.php << 'SCRIPT' +make(Illuminate\Contracts\Http\Kernel::class); +\$kernel->handle(\$request = Illuminate\Http\Request::capture()); +use Illuminate\Support\Facades\Storage; +\$data = Storage::disk('local')->get('esign/1/templates/7Vi374wZwcRg5bjlXPJiASkizxLSJlCNDJhaMBeo.pdf'); +header('Content-Type: text/plain'); +echo base64_encode(\$data); +SCRIPT +" +``` + +```bash +# curl로 base64 다운로드 → 디코딩 +curl -sk "https://mng.codebridge-x.com/_export_pdf.php" | base64 -d > /tmp/server_template.pdf +``` + +**결과:** +``` +-rw-r--r-- 1 aweso aweso 274929 Feb 20 08:37 /tmp/server_template.pdf +``` + +**핵심: curl → nginx → php-fpm(www-data) → Storage::get() 가능** + +### Step 3. 로컬 Docker에 파일 + DB INSERT + +```bash +# PDF를 Docker 컨테이너 안으로 복사 +docker cp /tmp/server_template.pdf sam-mng-1:/tmp/server_template.pdf +``` + +```bash +# tinker로 파일 저장 + DB INSERT (로컬 Docker는 www-data로 실행되므로 Storage 접근 가능) +docker exec sam-mng-1 php artisan tinker --execute=" +use Illuminate\Support\Facades\Storage; +use App\Models\ESign\EsignFieldTemplate; +use App\Models\ESign\EsignFieldTemplateItem; +use Illuminate\Support\Facades\DB; + +// PDF 저장 +\$data = file_get_contents('/tmp/server_template.pdf'); +Storage::disk('local')->put('esign/1/templates/7Vi374wZwcRg5bjlXPJiASkizxLSJlCNDJhaMBeo.pdf', \$data); + +// DB INSERT +DB::beginTransaction(); +\$t = EsignFieldTemplate::create([ + 'tenant_id' => 1, + 'name' => '영업파트너 계약서(단체용)', + 'description' => '30%, 3%의 요율이 적용됩니다.', + 'category' => '영업파트너', + 'signer_count' => 2, + 'file_path' => 'esign/1/templates/7Vi374wZwcRg5bjlXPJiASkizxLSJlCNDJhaMBeo.pdf', + 'file_name' => '영업파트너 위촉계약서(단체용).pdf', + 'file_hash' => 'dcf4309399b4161f2b58f7e86688026935c616a41217243c3b7d020d91529b12', + 'file_size' => 274929, + 'variables' => [...], // Step 1에서 추출한 JSON + 'is_active' => true, + 'created_by' => 1, +]); + +// 필드 아이템 INSERT (Step 1에서 추출한 JSON 사용) +foreach (\$items as \$item) { + \$item['template_id'] = \$t->id; // 새로 생성된 ID로 FK 연결 + EsignFieldTemplateItem::create(\$item); +} +DB::commit(); +" +``` + +**결과:** +``` +PDF saved: 274929 bytes +Template created: ID 7 +Items created: 13 +Done! +``` + +### Step 4. 임시 파일 정리 + +```bash +# 서버 임시 PHP 삭제 +ssh pro@114.203.209.83 "rm -f /home/webservice/mng/public/_export_pdf.php" + +# 로컬 임시 파일 삭제 +rm -f /tmp/server_template.pdf +docker exec sam-mng-1 rm -f /tmp/server_template.pdf +``` + +## 전체 흐름 요약 + +``` +[서버] [WSL 호스트] [로컬 Docker] + +1. tinker로 DB 데이터 추출 (JSON) +2. public/에 임시 PHP 배치 + (Storage::get → base64) + 3. curl로 base64 다운 ←── _export_pdf.php 실행 + base64 -d > /tmp/pdf + 4. docker cp ──→ /tmp/에 PDF 도착 + 5. tinker로 실행 + ├─ Storage::put (PDF) + └─ DB INSERT (Template + Items) + 6. 임시 파일 삭제 +``` + +## 로컬 → 서버와의 차이점 + +| 단계 | 로컬 → 서버 | 서버 → 로컬 | +|------|-------------|-------------| +| DB 추출 | `docker exec tinker` | `ssh tinker` | +| 파일 추출 | `docker cp` → WSL | `public/` PHP → `curl` → WSL | +| 파일 전송 | `base64` → SSH → `/tmp` | `curl` → `base64 -d` → `/tmp` | +| 파일 저장 | `public/` PHP (www-data) | `docker exec tinker` (www-data) | +| DB INSERT | `public/` PHP (www-data) | `docker exec tinker` (www-data) | + +**핵심 차이:** +- **서버에 쓸 때**: SSH로 storage 접근 불가 → `public/` PHP + curl 필수 +- **로컬에 쓸 때**: Docker tinker가 이미 www-data → tinker로 직접 가능 (PHP 스크립트 불필요) + +## 주의사항 + +- 서버 export PHP는 **인증 없이 접근 가능** → 파일 경로 노출 위험 → 즉시 삭제 +- 서버 template ID와 로컬 template ID는 다름 (auto_increment) → FK는 새 ID 사용 +- 대용량 파일(수십 MB)은 base64로 약 33% 증가 → curl 타임아웃 주의