diff --git a/changes/2025-12-15_items-api-files-fix.md b/changes/2025-12-15_items-api-files-fix.md new file mode 100644 index 0000000..e6a895d --- /dev/null +++ b/changes/2025-12-15_items-api-files-fix.md @@ -0,0 +1,300 @@ +# Items API files 배열 에러 수정 + +## 날짜 +2025-12-15 + +## 문제 +`PUT /api/v1/items/{id}` 요청 시 500 에러 발생 +``` +"Array to string conversion" +``` + +## 원인 분석 +1. API 요청에서 `files` 배열이 전송됨: +```json +{ + "files": { + "drawing": [{ + "id": 5, + "file_name": "IMG_2163.png", + "file_path": "287/items/2025/12/ec3483f4152d1eb1.png" + }] + } +} +``` + +2. `ItemsService::getKnownFields()`의 `$apiFields`에 `files`가 없어서 동적 필드로 인식됨 + +3. `extractDynamicOptions()`에서 `files`가 "알려지지 않은 필드"로 추출됨 + +4. `$product->update($data)` 호출 시 `files` 배열이 그대로 전달되어 DB 저장 시 에러 발생 + +## 수정 파일 +`api/app/Services/ItemsService.php` + +## 수정 내용 + +### 1. getKnownFields() 메서드 (라인 36-37) +```php +// 수정 전 +$apiFields = ['item_type', 'type_code', 'bom', 'product_type']; + +// 수정 후 +$apiFields = ['item_type', 'type_code', 'bom', 'product_type', 'files']; +``` + +### 2. updateProduct() 메서드 (라인 729-730) +```php +// 수정 전 +unset($data['item_type']); + +// 수정 후 +unset($data['item_type'], $data['files']); +``` + +### 3. updateMaterial() 메서드 (라인 771-772) +```php +// 수정 전 +unset($data['item_type'], $data['code']); + +// 수정 후 +unset($data['item_type'], $data['code'], $data['files']); +``` + +## 적용 체크리스트 +- [x] `getKnownFields()` - `$apiFields`에 `'files'` 추가 +- [x] `updateProduct()` - `unset()`에 `$data['files']` 추가 +- [x] `updateMaterial()` - `unset()`에 `$data['files']` 추가 + +## 커밋 정보 +``` +c68c280 fix: Items API 수정 시 files 배열로 인한 500 에러 수정 +``` + +## 관련 파일 +- `api/app/Http/Controllers/Api/V1/ItemsController.php` +- `api/app/Http/Controllers/Api/V1/ItemsFileController.php` + +--- + +# ItemsFileController delete 메서드 타입 에러 수정 + +## 날짜 +2025-12-15 + +## 문제 +`DELETE /api/v1/items/{id}/files/{fileId}` 요청 시 타입 에러 발생 +``` +Argument #2 ($fileId) must be of type int, string given +``` + +## 원인 분석 +Laravel 라우트 파라미터는 기본적으로 string으로 전달되는데, 컨트롤러 메서드에서 `int` 타입힌트를 사용하여 에러 발생 + +## 수정 파일 +`api/app/Http/Controllers/Api/V1/ItemsFileController.php` + +## 수정 내용 + +### delete() 메서드 (라인 157-159) +```php +// 수정 전 +public function delete(int $id, int $fileId, Request $request) +{ + return ApiResponse::handle(function () use ($id, $fileId, $request) { + +// 수정 후 +public function delete(int $id, mixed $fileId, Request $request) +{ + $fileId = (int) $fileId; + + return ApiResponse::handle(function () use ($id, $fileId, $request) { +``` + +## 적용 체크리스트 +- [x] `delete()` 메서드 - `$fileId` 파라미터 타입을 `mixed`로 변경 +- [x] `delete()` 메서드 - 내부에서 `$fileId = (int) $fileId;` 캐스팅 추가 + +## 커밋 정보 +``` +1040ce0 fix: ItemsFileController delete 메서드 타입 에러 수정 +``` + +--- + +# ItemsFileController userId null 처리 + +## 날짜 +2025-12-15 + +## 문제 +`DELETE /api/v1/items/{id}/files/{fileId}` 요청 시 500 에러 발생 +``` +softDeleteFile(): Argument #1 ($userId) must be of type int, null given +``` + +## 원인 분석 +- `auth()->id()`가 `null`을 반환 +- API 키 인증만 사용하고 Sanctum 토큰 인증이 없는 경우 발생 +- `softDeleteFile(int $userId)` 메서드에 null 전달 시 타입 에러 + +## 수정 파일 +`api/app/Http/Controllers/Api/V1/ItemsFileController.php` + +## 수정 내용 + +### 1. upload() 메서드 (라인 77) +```php +// 수정 전 +$userId = auth()->id(); + +// 수정 후 +$userId = auth()->id() ?? app('api_user'); +``` + +### 2. delete() 메서드 (라인 163) +```php +// 수정 전 +$userId = auth()->id(); + +// 수정 후 +$userId = auth()->id() ?? app('api_user'); +``` + +## 적용 체크리스트 +- [x] `upload()` 메서드 - `auth()->id() ?? app('api_user')` 변경 +- [x] `delete()` 메서드 - `auth()->id() ?? app('api_user')` 변경 + +## 커밋 정보 +``` +22abb99 fix: ItemsFileController userId null 처리 추가 +``` + +--- + +# ItemsFileController 파일 삭제 로직 일원화 + +## 날짜 +2025-12-15 + +## 문제 +- `upload()` 메서드의 파일 교체 삭제와 `delete()` 메서드의 파일 삭제 로직이 분리되어 있음 +- userId 캐스팅이 일관되지 않음 (upload에만 int 캐스팅 적용) +- 관리 포인트가 2곳으로 분산 + +## 수정 파일 +`api/app/Http/Controllers/Api/V1/ItemsFileController.php` + +## 수정 내용 + +### 1. deleteFile() private 메서드 추가 (라인 195-199) +```php +// 추가 +private function deleteFile(File $file): void +{ + $userId = (int) (auth()->id() ?? app('api_user')); + $file->softDeleteFile($userId); +} +``` + +### 2. upload() 메서드 - 기존 파일 교체 시 (라인 98-100) +```php +// 수정 전 +if ($existingFile) { + $existingFile->softDeleteFile($userId); + $replaced = true; +} + +// 수정 후 +if ($existingFile) { + $this->deleteFile($existingFile); + $replaced = true; +} +``` + +### 3. delete() 메서드 (라인 180-181) +```php +// 수정 전 +$userId = auth()->id() ?? app('api_user'); +... +$file->softDeleteFile($userId); + +// 수정 후 +// $userId 변수 제거 +$this->deleteFile($file); +``` + +## 적용 체크리스트 +- [x] `deleteFile()` private 메서드 추가 +- [x] `upload()` 메서드 - `$this->deleteFile($existingFile)` 사용 +- [x] `delete()` 메서드 - `$userId` 변수 제거, `$this->deleteFile($file)` 사용 + +## 커밋 정보 +``` +dea414b refactor: ItemsFileController 파일 삭제 로직 일원화 +``` + +--- + +# ItemsFileController 파일 다운로드 URL 수정 + +## 날짜 +2025-12-15 + +## 문제 +파일 다운로드 시 인증 오류 발생 +- 생성되는 URL: `/api/v1/files/download/{base64_path}` (라우트 없음) +- 실제 라우트: `/api/v1/files/{id}/download` + +## 수정 파일 +`api/app/Http/Controllers/Api/V1/ItemsFileController.php` + +## 수정 내용 + +### 1. getFileUrl() 메서드 (라인 244-247) +```php +// 수정 전 +private function getFileUrl(string $filePath): string +{ + return url('/api/v1/files/download/'.base64_encode($filePath)); +} + +// 수정 후 +private function getFileUrl(int $fileId): string +{ + return url("/api/v1/files/{$fileId}/download"); +} +``` + +### 2. formatFileResponse() 메서드 (라인 232) +```php +// 수정 전 +'file_url' => $this->getFileUrl($file->file_path), + +// 수정 후 +'file_url' => $this->getFileUrl($file->id), +``` + +### 3. upload() 응답 (라인 142) +```php +// 수정 전 +'file_url' => $this->getFileUrl($filePath), + +// 수정 후 +'file_url' => $this->getFileUrl($file->id), +``` + +## 적용 체크리스트 +- [x] `getFileUrl()` 메서드 - 파라미터를 `string $filePath` → `int $fileId`로 변경 +- [x] `getFileUrl()` 메서드 - URL 형식을 `/api/v1/files/{id}/download`로 변경 +- [x] `formatFileResponse()` - `$this->getFileUrl($file->id)` 사용 +- [x] `upload()` 응답 - `$this->getFileUrl($file->id)` 사용 + +## 프론트엔드 참고 +- 다운로드 요청 시 **API 키 헤더 필수** (`X-API-Key` 또는 설정된 헤더) +- 기존 FileStorageController의 download 라우트 활용 + +## 커밋 정보 +``` +98262ed fix: ItemsFileController 파일 다운로드 URL을 file_id 기반으로 변경 +``` diff --git a/plans/items-table-unification-plan.md b/plans/items-table-unification-plan.md index 244778b..eee1f67 100644 --- a/plans/items-table-unification-plan.md +++ b/plans/items-table-unification-plan.md @@ -116,9 +116,9 @@ DELETE FROM products WHERE product_type NOT IN ('FG', 'PT'); ### 0.3 체크리스트 -- [ ] products 비표준 타입 삭제 -- [ ] 관련 BOM 데이터 정리 -- [ ] 삭제 건수 확인 +- [x] products 비표준 타입 삭제 +- [x] 관련 BOM 데이터 정리 +- [x] 삭제 건수 확인 --- @@ -233,11 +233,11 @@ DB::statement(" ### 1.5 체크리스트 -- [ ] items 마이그레이션 생성 -- [ ] item_details 마이그레이션 생성 -- [ ] item_attributes 마이그레이션 생성 -- [ ] 데이터 이관 스크립트 실행 -- [ ] 건수 검증 (1,225건) +- [x] items 마이그레이션 생성 +- [x] item_details 마이그레이션 생성 +- [x] item_attributes 마이그레이션 생성 +- [x] 데이터 이관 스크립트 실행 +- [x] 건수 검증 (1,225건) --- @@ -305,11 +305,11 @@ class ItemService extends Service ### 2.3 체크리스트 -- [ ] Item 모델 생성 -- [ ] ItemDetail 모델 생성 -- [ ] ItemAttribute 모델 생성 -- [ ] ItemService 생성 -- [ ] ItemRequest 생성 +- [x] Item 모델 생성 +- [x] ItemDetail 모델 생성 +- [x] ItemAttribute 모델 생성 +- [x] ItemService 생성 +- [x] ItemRequest 생성 --- @@ -341,9 +341,9 @@ UPDATE item_pages SET source_table = 'items' WHERE source_table IN ('products', ### 3.3 체크리스트 -- [ ] ItemPage 모델 수정 (getTargetModelClass) -- [ ] item_pages.source_table 마이그레이션 -- [ ] ItemMasterService 연동 테스트 +- [x] ItemPage 모델 수정 (getTargetModelClass) +- [x] item_pages.source_table 마이그레이션 +- [x] ItemMasterService 연동 테스트 --- @@ -401,11 +401,11 @@ Route::prefix('items')->group(function () { ### 4.4 체크리스트 -- [ ] ItemController 생성 -- [ ] ItemIndexRequest, ItemStoreRequest 등 생성 -- [ ] 라우트 등록 -- [ ] Swagger 문서 작성 -- [ ] 기존 ProductController, MaterialController 제거 +- [x] ItemController 생성 +- [x] ItemIndexRequest, ItemStoreRequest 등 생성 +- [x] 라우트 등록 +- [x] Swagger 문서 작성 +- [x] 기존 ProductController, MaterialController 제거 --- @@ -426,9 +426,9 @@ Route::prefix('items')->group(function () { ### 5.2 체크리스트 -- [ ] 각 참조 테이블 마이그레이션 작성 -- [ ] 관련 모델 관계 업데이트 -- [ ] 데이터 검증 +- [x] 각 참조 테이블 마이그레이션 작성 +- [x] 관련 모델 관계 업데이트 +- [x] 데이터 검증 --- @@ -436,14 +436,14 @@ Route::prefix('items')->group(function () { ### 6.1 체크리스트 -- [ ] CRUD 테스트 (전체 item_type) -- [ ] BOM 계산 테스트 -- [ ] Item-Master 연동 테스트 -- [ ] 참조 무결성 테스트 -- [ ] products 테이블 삭제 -- [ ] materials 테이블 삭제 -- [ ] 기존 Product, Material 모델 삭제 -- [ ] 기존 ProductService, MaterialService 삭제 +- [x] CRUD 테스트 (전체 item_type) +- [x] BOM 계산 테스트 +- [x] Item-Master 연동 테스트 +- [x] 참조 무결성 테스트 +- [x] products 테이블 삭제 +- [x] materials 테이블 삭제 +- [x] 기존 Product, Material 모델 삭제 +- [x] 기존 ProductService, MaterialService 삭제 --- @@ -553,13 +553,16 @@ $children = Item::whereIn('id', $childIds)->get()->keyBy('id'); | Phase | 작업 | 상태 | |-------|------|------| -| 0 | 데이터 정규화 (비표준 item_type/BOM 삭제) | ⬜ | -| 1 | items 테이블 생성 + 데이터 이관 | ⬜ | -| 2 | Item 모델 + Service 생성 | ⬜ | -| 3 | Item-Master 연동 수정 | ⬜ | -| 4 | API 통합 | ⬜ | -| 5 | 참조 테이블 마이그레이션 | ⬜ | -| 6 | 정리 | ⬜ | +| 0 | 데이터 정규화 (비표준 item_type/BOM 삭제) | ✅ 완료 | +| 1 | items 테이블 생성 + 데이터 이관 | ✅ 완료 | +| 2 | Item 모델 + Service 생성 | ✅ 완료 | +| 3 | Item-Master 연동 수정 | ✅ 완료 | +| 4 | API 통합 | ✅ 완료 | +| 5 | 참조 테이블 마이그레이션 | ✅ 완료 | +| 6 | 정리 | ✅ 완료 | + +> **완료일**: 2025-12-15 +> **관련 커밋**: `039fd62` (products/materials 테이블 삭제), `a93dfe7` (Phase 6 완료) ---