diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md new file mode 100644 index 00000000..4c50fb77 --- /dev/null +++ b/docs/TROUBLESHOOTING.md @@ -0,0 +1,311 @@ +# Troubleshooting Guide + +프로젝트에서 발생한 오류와 해결 방법을 정리한 문서입니다. + +--- + +## 📋 목차 + +1. [HTMX 관련 오류](#htmx-관련-오류) +2. [Laravel FormRequest 오류](#laravel-formrequest-오류) +3. [데이터베이스 제약 오류](#데이터베이스-제약-오류) + +--- + +## HTMX 관련 오류 + +### 1. HTMX 요청이 잘못된 URL로 전송되는 문제 + +**증상:** +``` +Request URL: https://mng.sam.kr/tenants/285/edit +Expected URL: https://mng.sam.kr/api/admin/tenants/285 +``` + +폼에 `hx-put="/api/admin/tenants/{{ $id }}"`로 설정했는데도 현재 페이지 URL로 요청이 전송됨. + +**원인:** +HTMX가 상대 경로를 현재 페이지 기준으로 해석 + +**해결 방법:** +`url()` 헬퍼 함수로 전체 URL 사용 + +```blade + +
+ + + +``` + +**관련 파일:** +- `resources/views/tenants/edit.blade.php` + +**커밋:** `bc3777a` + +--- + +### 2. HTMX DELETE 요청 시 419 CSRF 토큰 오류 + +**증상:** +``` +DELETE https://mng.sam.kr/api/admin/tenants/284 +Status Code: 419 unknown status +``` + +**원인:** +`htmx.ajax()` 호출 시 CSRF 토큰 헤더 누락 + +**해결 방법:** +HTMX ajax 요청에 CSRF 토큰 헤더 추가 + +```javascript +// ❌ 잘못된 방법 +htmx.ajax('DELETE', `/api/admin/tenants/${id}`, { + target: '#tenant-table', + swap: 'none' +}); + +// ✅ 올바른 방법 +htmx.ajax('DELETE', `/api/admin/tenants/${id}`, { + target: '#tenant-table', + swap: 'none', + headers: { + 'X-CSRF-TOKEN': '{{ csrf_token() }}' + } +}); +``` + +**관련 파일:** +- `resources/views/tenants/index.blade.php` + +**커밋:** `bc3777a` + +--- + +### 3. ViewServiceProvider가 변수를 덮어쓰는 문제 + +**증상:** +``` +Method Illuminate\Database\Eloquent\Collection::hasPages does not exist. +``` + +컨트롤러에서 `LengthAwarePaginator`를 전달했는데 뷰에서 `Collection`으로 변경됨. + +**원인:** +`ViewServiceProvider`의 글로벌 View Composer가 모든 뷰(`*`)에 같은 변수명으로 데이터 주입 + +```php +// ViewServiceProvider.php +View::composer('*', function ($view) { + $tenants = Tenant::active()->get(); // Collection 반환 + $view->with('tenants', $tenants); // 컨트롤러의 $tenants를 덮어씀 +}); +``` + +**해결 방법:** +전역 View Composer의 변수명을 다르게 지정 + +```php +// ✅ 올바른 방법 +View::composer('*', function ($view) { + $globalTenants = Tenant::active()->get(); + $view->with('globalTenants', $globalTenants); +}); +``` + +**관련 파일:** +- `app/Providers/ViewServiceProvider.php` +- `resources/views/partials/tenant-selector.blade.php` + +**커밋:** `f49cfd9` + +--- + +## Laravel FormRequest 오류 + +### 4. FormRequest 유효성 검증 실패로 인한 302 리다이렉트 + +**증상:** +``` +PUT https://mng.sam.kr/api/admin/tenants/285 +Status Code: 302 Found +Location: https://mng.sam.kr/tenants/285/edit +``` + +API 엔드포인트가 JSON 대신 HTML 리다이렉트 반환. + +**원인:** +`UpdateTenantRequest`에서 라우트 파라미터 이름이 잘못됨 + +```php +// routes/api.php +Route::put('/{id}', [TenantController::class, 'update']); + +// UpdateTenantRequest.php +$tenantId = $this->route('tenant'); // ❌ 'tenant'는 존재하지 않음 +``` + +라우트 파라미터는 `{id}`인데 `route('tenant')`로 가져오려 해서 `null` 반환. +→ `code` 유일성 검증이 자기 자신을 제외하지 못함 +→ 유효성 검증 실패 +→ Laravel이 자동으로 이전 페이지로 리다이렉트 + +**해결 방법:** +라우트 파라미터 이름을 정확히 매칭 + +```php +// ✅ 올바른 방법 +$tenantId = $this->route('id'); + +return [ + 'code' => [ + 'required', + 'string', + 'max:50', + Rule::unique('tenants', 'code')->ignore($tenantId), + ], +]; +``` + +**관련 파일:** +- `app/Http/Requests/UpdateTenantRequest.php` +- `routes/api.php` + +**커밋:** `bc3777a` + +--- + +## 데이터베이스 제약 오류 + +### 5. 외래키 제약으로 인한 영구 삭제 실패 + +**증상:** +``` +SQLSTATE[23000]: Integrity constraint violation: 1451 +Cannot delete or update a parent row: a foreign key constraint fails +(`samdb`.`user_tenants`, CONSTRAINT `user_tenants_tenant_id_foreign` +FOREIGN KEY (`tenant_id`) REFERENCES `tenants` (`id`)) +``` + +**원인:** +테넌트를 삭제하려 하는데, `user_tenants` 테이블에 아직 이 테넌트를 참조하는 레코드가 남아있음. + +**해결 방법:** +영구 삭제 전에 관련 데이터를 먼저 삭제 + +```php +public function forceDeleteTenant(int $id): bool +{ + $tenant = Tenant::withTrashed()->findOrFail($id); + + // ✅ 관련 데이터 먼저 삭제 + $tenant->users()->detach(); // user_tenants 관계 삭제 + $tenant->departments()->forceDelete(); // 부서 영구 삭제 + $tenant->menus()->forceDelete(); // 메뉴 영구 삭제 + $tenant->roles()->forceDelete(); // 역할 영구 삭제 + + return $tenant->forceDelete(); +} +``` + +**관련 파일:** +- `app/Services/TenantService.php` + +**커밋:** `bc3777a` + +**참고:** +- `detach()`: Many-to-Many 관계 삭제 (pivot 테이블) +- `forceDelete()`: SoftDelete 무시하고 영구 삭제 + +--- + +## 디버깅 팁 + +### 1. HTMX 요청 디버깅 + +**브라우저 개발자 도구:** +``` +Network 탭 → Request URL, Method, Status Code 확인 +Headers 탭 → Request/Response Headers 확인 +Payload 탭 → 전송된 데이터 확인 +Response 탭 → 서버 응답 내용 확인 +``` + +**HTMX 이벤트 로깅:** +```javascript +document.body.addEventListener('htmx:configRequest', function(evt) { + console.log('HTMX Request:', evt.detail); +}); + +document.body.addEventListener('htmx:afterRequest', function(evt) { + console.log('HTMX Response:', evt.detail); +}); +``` + +### 2. Laravel 로그 확인 + +```bash +# 최근 로그 확인 +tail -50 storage/logs/laravel.log + +# 에러만 필터링 +tail -100 storage/logs/laravel.log | grep "local.ERROR" + +# 실시간 로그 모니터링 +tail -f storage/logs/laravel.log +``` + +### 3. 캐시 클리어 + +```bash +# 뷰 캐시 삭제 +php artisan view:clear + +# 전체 캐시 삭제 +php artisan cache:clear +php artisan config:clear +php artisan route:clear + +# 컴파일된 뷰 파일 수동 삭제 +rm -rf storage/framework/views/*.php + +# Docker 컨테이너의 OPcache 클리어 +docker exec sam-mng-1 php -r "opcache_reset();" +``` + +--- + +## 예방 체크리스트 + +### HTMX 사용 시 +- [ ] CSRF 토큰 헤더 추가 (`X-CSRF-TOKEN`) +- [ ] 전체 URL 사용 (`url()` 헬퍼) +- [ ] 올바른 HTTP 메서드 사용 (GET, POST, PUT, DELETE) +- [ ] 응답 처리 이벤트 리스너 등록 + +### FormRequest 작성 시 +- [ ] 라우트 파라미터 이름 확인 +- [ ] 유일성 검증 시 현재 레코드 제외 (`ignore()`) +- [ ] 날짜 필드는 `nullable` + `date` 검증 + +### 데이터 삭제 시 +- [ ] 외래키 관계 확인 +- [ ] 관련 데이터 먼저 삭제 +- [ ] SoftDelete vs ForceDelete 구분 +- [ ] 트랜잭션 사용 고려 + +### 캐시 관련 +- [ ] 코드 변경 후 캐시 클리어 +- [ ] 뷰 수정 후 `view:clear` +- [ ] 설정 변경 후 `config:clear` + +--- + +## 참고 링크 + +- [Laravel Validation](https://laravel.com/docs/12.x/validation) +- [Laravel Eloquent Relationships](https://laravel.com/docs/12.x/eloquent-relationships) +- [HTMX Documentation](https://htmx.org/docs/) +- [SAM API Rules](../docs/reference/api-rules.md) +- [Quality Checklist](../docs/reference/quality-checklist.md) \ No newline at end of file