docs: 트러블슈팅 가이드 추가
테넌트 관리 개발 중 발생한 주요 오류와 해결 방법 문서화: - HTMX 관련 오류 (URL 라우팅, CSRF, ViewComposer 충돌) - FormRequest 유효성 검증 오류 - 데이터베이스 외래키 제약 오류 디버깅 팁 및 예방 체크리스트 포함
This commit is contained in:
311
docs/TROUBLESHOOTING.md
Normal file
311
docs/TROUBLESHOOTING.md
Normal file
@@ -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
|
||||
<!-- ❌ 잘못된 방법 -->
|
||||
<form hx-put="/api/admin/tenants/{{ $id }}">
|
||||
|
||||
<!-- ✅ 올바른 방법 -->
|
||||
<form hx-put="{{ url('/api/admin/tenants/' . $id) }}">
|
||||
```
|
||||
|
||||
**관련 파일:**
|
||||
- `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)
|
||||
Reference in New Issue
Block a user