# 서버 작업 이력 > 서버: mng.codebridge-x.com (114.203.209.83) > 접속 계정: pro / sampass --- ## 2026-01-30 ### 1. Nginx client_max_body_size 설정 (413 오류 해결) **문제**: 명함 이미지 업로드 시 `413 Content Too Large` 오류 발생 **작업 내용**: ```bash sudo nano /etc/nginx/sites-available/codebridge-x ``` `mng.codebridge-x.com` 서버 블록에 추가: ```nginx server { server_name mng.codebridge-x.com; client_max_body_size 20M; # 추가됨 ... } ``` ```bash sudo nginx -t sudo systemctl reload nginx ``` --- ### 2. shared-storage 디렉토리 생성 **문제**: `Unable to create a directory at /var/www/shared-storage/tenants` 오류 발생 - 서버 환경은 Docker가 아니므로 `/var/www/` 경로가 존재하지 않음 **작업 내용**: ```bash sudo mkdir -p /home/webservice/shared-storage/tenants sudo chown -R www-data:www-data /home/webservice/shared-storage sudo chmod -R 775 /home/webservice/shared-storage ``` **결과 확인**: ```bash ls -la /home/webservice/shared-storage/ # drwxrwsr-x 3 www-data www-data 4096 Jan 30 14:19 . # drwxrwsr-x 2 www-data www-data 4096 Jan 30 14:19 tenants ``` --- ### 3. MNG .env 파일에 TENANT_STORAGE_PATH 추가 **작업 내용**: ```bash # .env 파일에 추가 echo 'TENANT_STORAGE_PATH=/home/webservice/shared-storage/tenants' >> /home/webservice/mng/.env ``` **설정 확인**: ```bash grep TENANT_STORAGE /home/webservice/mng/.env # TENANT_STORAGE_PATH=/home/webservice/shared-storage/tenants ``` --- ### 4. Laravel 캐시 클리어 및 PHP-FPM 재시작 **작업 내용**: ```bash cd /home/webservice/mng php artisan config:clear php artisan cache:clear php artisan route:clear php artisan view:clear sudo systemctl restart php8.4-fpm ``` **설정 반영 확인**: ```bash php artisan tinker --execute="echo config('filesystems.disks.tenant.root');" # /home/webservice/shared-storage/tenants ``` --- ### 5. AI 설정 메뉴 추가 **문제**: 로컬에는 있던 "AI 설정" 메뉴가 서버에 없음 **작업 내용** (tinker 사용): ```bash cd /home/webservice/mng php artisan tinker --execute=' use App\Models\Commons\Menu; $s = Menu::where("tenant_id", 1)->where("name", "시스템")->first(); if ($s) { $a = Menu::where("tenant_id", 1)->where("parent_id", $s->id)->where("url", "/system/ai-config")->first(); if ($a) { echo "AI menu exists: " . $a->id; } else { $n = Menu::create([ "tenant_id" => 1, "parent_id" => $s->id, "name" => "AI 설정", "url" => "/system/ai-config", "is_active" => true, "sort_order" => 3, "hidden" => false, "icon" => "cpu" ]); echo "Created: " . $n->id; } } ' ``` --- ### 6. AI Config 데이터 추가 (Gemini, GCS) **작업 내용** (tinker 사용): ```bash cd /home/webservice/mng php artisan tinker --execute=' use App\Models\System\AiConfig; $configs = [ [ "name" => "Gemini for Google Cloud API", "provider" => "gemini", "api_key" => "vertex_ai_service_account", "model" => "gemini-2.0-flash", "base_url" => "https://generativelanguage.googleapis.com/v1beta", "description" => null, "is_active" => true, "options" => [ "region" => "us-central1", "auth_type" => "vertex_ai", "project_id" => "codebridge-chatbot", "service_account_path" => "/home/webservice/sales/apikey/google_service_account.json" ], ], [ "name" => "Production GCS", "provider" => "gcs", "api_key" => "gcs_service_account", "model" => "-", "base_url" => "https://storage.googleapis.com", "description" => "음성 녹음 파일 백업용 (본사 연구)", "is_active" => true, "options" => [ "bucket_name" => "codebridge-speech-audio-files", "service_account_path" => "/home/webservice/sales/apikey/google_service_account.json" ], ], ]; foreach ($configs as $config) { $existing = AiConfig::where("name", $config["name"])->first(); if ($existing) { echo "Already exists: " . $config["name"] . "\n"; } else { AiConfig::create($config); echo "Created: " . $config["name"] . "\n"; } } ' ``` --- ### 7. AI Config service_account_path 경로 수정 **문제**: Docker 환경 경로 `/var/www/sales/apikey/...`가 서버에서 작동하지 않음 **작업 내용** (tinker 사용): ```bash cd /home/webservice/mng php artisan tinker --execute=' use App\Models\System\AiConfig; $configs = AiConfig::all(); foreach ($configs as $config) { $options = $config->options ?? []; if (isset($options["service_account_path"])) { $oldPath = $options["service_account_path"]; $newPath = str_replace("/var/www/sales/", "/home/webservice/sales/", $oldPath); if ($oldPath !== $newPath) { $options["service_account_path"] = $newPath; $config->options = $options; $config->save(); echo "Updated " . $config->name . ": " . $newPath . "\n"; } } } echo "Done.\n"; ' ``` --- ### 8. SalesRoleSeeder 실행 **문제**: 서버에 영업 관련 역할(권한)이 없음 **작업 내용**: ```bash cd /home/webservice/mng php artisan db:seed --class=SalesRoleSeeder ``` --- ### 9. tenant-storage 심볼릭 링크 수정 **문제**: 명함 이미지가 저장되었지만 브라우저에서 로드되지 않음 - 기존 심볼릭 링크가 잘못된 경로를 가리킴: `/var/www/api/storage/app/tenants` (존재하지 않음) **작업 내용**: ```bash cd /home/webservice/mng/public rm tenant-storage ln -s /home/webservice/shared-storage/tenants tenant-storage ``` **결과 확인**: ```bash ls -la tenant-storage # lrwxrwxrwx 1 pro develop 39 Jan 30 14:35 tenant-storage -> /home/webservice/shared-storage/tenants ``` --- ### 10. sales_scenario_checklists 테이블 스키마 동기화 **문제**: 로컬과 서버의 테이블 구조 불일치 (컬럼 및 인덱스 누락) **추가된 컬럼**: ```sql ALTER TABLE sales_scenario_checklists ADD COLUMN scenario_type ENUM('sales','manager') NOT NULL DEFAULT 'sales' COMMENT '시나리오 유형' AFTER tenant_id; ALTER TABLE sales_scenario_checklists ADD COLUMN checkpoint_id VARCHAR(50) DEFAULT NULL COMMENT '체크포인트 ID' AFTER step_id; ALTER TABLE sales_scenario_checklists ADD COLUMN checked_at TIMESTAMP NULL DEFAULT NULL COMMENT '체크 일시' AFTER is_checked; ALTER TABLE sales_scenario_checklists ADD COLUMN checked_by BIGINT UNSIGNED DEFAULT NULL COMMENT '체크한 사용자 ID' AFTER checked_at; ALTER TABLE sales_scenario_checklists ADD COLUMN memo TEXT COMMENT '메모' AFTER checked_by; ``` **추가된 인덱스**: ```sql ALTER TABLE sales_scenario_checklists ADD UNIQUE KEY sales_scenario_checkpoint_unique (tenant_id, scenario_type, step_id, checkpoint_id); ALTER TABLE sales_scenario_checklists ADD INDEX sales_scenario_checklists_tenant_id_scenario_type_index (tenant_id, scenario_type); ``` **작업 방법** (tinker 사용): ```bash cd /home/webservice/mng php artisan tinker --execute="DB::statement('ALTER TABLE ...');" ``` --- ### 11. 마이그레이션 파일 수정 (서버 호환성) **문제**: 서버에서 git push 후 자동 마이그레이션 실행 시 여러 오류 발생 **수정된 마이그레이션 파일들**: #### 1) `2026_01_29_090000_fix_sales_scenario_checklists_unique_key.php` - **오류**: `Can't DROP 'sales_scenario_unique'; check that column/key exists` - **원인**: 서버에 해당 인덱스가 없음 (이미 수동으로 다른 이름으로 생성됨) - **해결**: 인덱스 삭제/생성 전 `SHOW INDEX` 쿼리로 존재 여부 확인 ```php // 수정 전 $table->dropUnique('sales_scenario_unique'); // 수정 후 $indexes = DB::select("SHOW INDEX FROM sales_scenario_checklists WHERE Key_name = 'sales_scenario_unique'"); if (count($indexes) > 0) { $table->dropUnique('sales_scenario_unique'); } ``` #### 2) `2026_01_29_093000_add_gcs_uri_to_sales_consultations.php` - **오류**: `Table 'sam.sales_consultations' doesn't exist` - **원인**: 테이블 생성 마이그레이션보다 먼저 실행됨 - **해결**: `Schema::hasTable()` 체크 추가 ```php public function up(): void { if (!Schema::hasTable('sales_consultations')) { return; } // ... 기존 로직 } ``` #### 3) `2026_01_29_100200_create_sales_scenario_checklists_table.php` - **오류**: `Table 'sales_scenario_checklists' already exists` - **원인**: 서버에 이미 테이블이 존재 (수동 생성됨) - **해결**: `Schema::hasTable()` 체크 추가 ```php public function up(): void { if (Schema::hasTable('sales_scenario_checklists')) { return; } Schema::create('sales_scenario_checklists', function (Blueprint $table) { // ... }); } ``` #### 4) `2026_01_30_150000_add_missing_columns_to_sales_scenario_checklists_table.php` - **오류**: `Method getDoctrineSchemaManager does not exist` - **원인**: Laravel 12에서 Doctrine DBAL 제거됨 - **해결**: `DB::select("SHOW INDEX ...")` 쿼리로 대체 ```php // 수정 전 (Laravel 11 이하) $sm = Schema::getConnection()->getDoctrineSchemaManager(); $indexes = $sm->listTableIndexes('sales_scenario_checklists'); // 수정 후 (Laravel 12 호환) $uniqueExists = DB::select("SHOW INDEX FROM sales_scenario_checklists WHERE Key_name = 'sales_scenario_checkpoint_unique'"); if (empty($uniqueExists)) { // 인덱스 추가 } ``` --- ## 참고: 마이그레이션 작성 가이드 (Laravel 12) ### 안전한 마이그레이션 패턴 ```php use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; // 1. 테이블 존재 여부 확인 if (Schema::hasTable('table_name')) { ... } // 2. 컬럼 존재 여부 확인 if (Schema::hasColumn('table_name', 'column_name')) { ... } // 3. 인덱스 존재 여부 확인 (Laravel 12) $indexes = DB::select("SHOW INDEX FROM table_name WHERE Key_name = 'index_name'"); if (empty($indexes)) { ... } ``` ### 주의사항 - Laravel 12에서 `getDoctrineSchemaManager()` 사용 불가 - 테이블/컬럼 생성 전 항상 존재 여부 체크 - 인덱스 삭제 전 항상 존재 여부 체크 --- ### 12. 배포 스크립트 수정 (심볼릭 링크 자동 복원) **문제**: git push 후 자동 배포 시 `git checkout .` 명령으로 인해 심볼릭 링크가 원래 값으로 덮어씌워짐 **수정 파일**: `/home/webservice/script/pull_mng.sh` **추가된 코드**: ```bash # 심볼릭 링크 수정 (서버 경로에 맞게) echo "심볼릭 링크 수정 중..." >> $LOGFILE cd /home/webservice/mng/public rm -f tenant-storage ln -s /home/webservice/shared-storage/tenants tenant-storage echo "심볼릭 링크 수정 완료" >> $LOGFILE ``` **효과**: 이제 git push 후에도 `tenant-storage` 심볼릭 링크가 자동으로 `/home/webservice/shared-storage/tenants`를 가리키게 됨 --- ### 13. 영업관리 - 대시보드 메뉴 추가 **문제**: 로컬에는 있는 "영업관리 - 대시보드" 메뉴가 서버에 없음 **작업 내용** (tinker 사용): ```bash cd /home/webservice/mng php artisan tinker --execute=' use App\Models\Commons\Menu; $sales = Menu::where("tenant_id", 1)->where("name", "영업관리")->first(); if ($sales) { $m = Menu::create([ "tenant_id" => 1, "parent_id" => $sales->id, "name" => "대시보드", "url" => "/sales/salesmanagement/dashboard", "is_active" => true, "sort_order" => 0, "hidden" => false, "icon" => "chart-bar" ]); echo "Created: " . $m->id; } ' ``` --- ## 참고: Docker vs 서버 경로 차이 | 항목 | Docker (로컬) | 서버 | |------|--------------|------| | 웹 루트 | `/var/www/` | `/home/webservice/` | | MNG 앱 | `/var/www/mng/` | `/home/webservice/mng/` | | API 앱 | `/var/www/api/` | `/home/webservice/api/` | | Sales 앱 | `/var/www/sales/` | `/home/webservice/sales/` | | 공유 스토리지 | `/var/www/shared-storage/` | `/home/webservice/shared-storage/` | 서버에 새로운 설정을 추가할 때는 경로 차이를 반드시 확인해야 합니다. --- ### 14. 영업관리 - 대시보드 메뉴 정렬 순서 수정 **문제**: 대시보드 메뉴가 "영업담당자 관리" 아래에 표시됨 (가장 위에 있어야 함) **작업 내용** (tinker 사용): ```bash cd /home/webservice/mng php artisan tinker --execute=' use App\Models\Commons\Menu; // 대시보드를 sort_order -1로 설정 (가장 위) Menu::where("tenant_id", 1)->where("parent_id", 15385)->where("url", "/sales/salesmanagement/dashboard")->update(["sort_order" => -1]); // 결과 확인 $children = Menu::where("tenant_id", 1)->where("parent_id", 15385)->orderBy("sort_order")->get(); foreach ($children as $c) { echo $c->name . " | sort:" . $c->sort_order . PHP_EOL; } ' ``` **결과**: ``` 대시보드 | sort:-1 영업담당자 관리 | sort:0 가망고객 관리 | sort:1 영업실적 관리 | sort:2 ``` --- ### 15. 영업관리 - 상품관리 메뉴 추가 **문제**: 로컬에는 있는 "영업관리 - 상품관리" 메뉴가 서버에 없음 **작업 내용** (tinker 사용): ```bash cd /home/webservice/mng php artisan tinker --execute=' use App\Models\Commons\Menu; $parentId = 15385; $m = Menu::create([ "tenant_id" => 1, "parent_id" => $parentId, "name" => "상품관리", "url" => "/sales/products", "is_active" => true, "sort_order" => 4, "hidden" => false, "icon" => "cube" ]); echo "Created: " . $m->id; ' ``` **결과**: Menu ID 15400 생성 --- ### 16. 상품 데이터 추가 (카테고리 + 상품) **문제**: 서버에 상품 데이터가 없음 (로컬에만 존재) **작업 내용** (tinker 사용): ```bash cd /home/webservice/mng php artisan tinker --execute=' use Illuminate\Support\Facades\DB; // 카테고리 추가 $categories = [ ["id" => 5, "code" => "MANUFACTURER", "name" => "제조 업체", ...], ["id" => 6, "code" => "CONSTRUCTION", "name" => "공사 업체", ...], ]; foreach ($categories as $cat) { DB::table("sales_product_categories")->insert($cat); } // 상품 추가 (11개) $products = [ // 제조업 상품 8개 (MFG_BASIC, MFG_QUALITY, MFG_PROCESS_ADD, MFG_AI, MFG_PHOTO, MFG_INVOICE_CARD, MFG_EQUIPMENT, MFG_RND) // 공사업 상품 3개 (CON_BASIC, CON_AI, CON_PHOTO) ]; foreach ($products as $prod) { DB::table("sales_products")->insert($prod); } ' ``` **결과**: - 카테고리 2개 생성 (제조 업체, 공사 업체) - 상품 11개 생성 (제조업 8개, 공사업 3개)