diff --git a/INDEX.md b/INDEX.md index 4eaa014..88eb469 100644 --- a/INDEX.md +++ b/INDEX.md @@ -15,7 +15,7 @@ | **Git 커밋** | `standards/git-conventions.md` | 커밋 메시지, 브랜치 전략 | | **품질 검증** | `standards/quality-checklist.md` | 코드 품질 체크리스트 | | **Swagger 작성** | `guides/swagger-guide.md` | API 문서 작성 방법 | -| **품목관리** | `specs/item-master-integration.md` | 품목 시스템 스펙 | +| **품목관리** | `rules/item-policy.md` | 품목 정책 (유형, 예약어, API 규칙) | | **게시판** | `specs/board-system-spec.md` | 게시판 시스템 설계 | | **단가관리** | `rules/pricing-policy.md` | 원가/판매가 계산, 리비전 관리 | | **MES 개발** | `projects/mes/README.md` | MES 프로젝트 개요 | @@ -68,6 +68,7 @@ docs/ | 문서 | 설명 | 필수 확인 시점 | |------|------|--------------| | [README.md](rules/README.md) | 비즈니스 규칙 개요 | 도메인 로직 구현 전 | +| [item-policy.md](rules/item-policy.md) | 품목 정책 (유형 체계, 예약어, API 규칙) | 품목 관련 작업 전 | | [pricing-policy.md](rules/pricing-policy.md) | 단가 정책 (원가/판매가 계산, 리비전 관리) | 단가 관련 작업 전 | ### specs/ - 기술 스펙 @@ -77,10 +78,7 @@ docs/ |------|------|--------------| | [database-schema.md](specs/database-schema.md) | DB 구조 및 관계도 | DB 변경 전 | | [board-system-spec.md](specs/board-system-spec.md) | 게시판 시스템 설계 | 게시판 작업 전 | -| [**ITEM-MASTER-INDEX.md**](specs/ITEM-MASTER-INDEX.md) | **품목관리 문서 인덱스 (개발 현황)** | **품목 작업 전 필수** | | [item-master-integration.md](specs/item-master-integration.md) | 품목관리 연동 설계 | 품목 연동 구현 시 | -| [item-master-field-key-validation.md](specs/item-master-field-key-validation.md) | ItemMaster 필드 키 검증 | 품목 필드 작업 전 | -| [item-master-field-integration.md](specs/item-master-field-integration.md) | ItemMaster 필드 통합 계획 | 품목 필드 통합 시 | | [docker-setup.md](specs/docker-setup.md) | Docker 환경 구성 | 환경 설정 시 | | [remote-work-setup.md](specs/remote-work-setup.md) | 원격 개발 설정 | 원격 작업 시 | @@ -189,6 +187,11 @@ API Flow Tester에서 생성되는 JSON 파일 저장 경로 ## 🔄 문서 구조 변경 이력 +- **2025-12-09**: 품목 정책 통합 문서 생성 + - `rules/item-policy.md` 생성 (4개 문서 통합) + - 삭제: `specs/ITEM-MASTER-INDEX.md`, `specs/item-master-field-key-validation.md`, `specs/item-master-field-integration.md`, `plans/items-api-unified-plan.md` + - 품목 관련 정책을 rules/ 디렉토리로 이동 + - **2025-12-09**: Item Master 문서 정리 및 인덱스 생성 - `specs/ITEM-MASTER-INDEX.md` 생성 (개발 현황/필요 항목 정리) - `history/2025-11/item-master-archived/` 생성 (구버전 문서 아카이브) diff --git a/plans/items-api-unified-plan.md b/plans/items-api-unified-plan.md deleted file mode 100644 index a18f92f..0000000 --- a/plans/items-api-unified-plan.md +++ /dev/null @@ -1,1095 +0,0 @@ -# Items API 통합 개발 계획 - -> Items API 명명 체계 통일 + Material CRUD 지원 추가 - -**작성일**: 2025-12-09 -**상태**: 진행 예정 - ---- - -## 1. 개요 - -### 1.1 목표 - -1. **명명 체계 통일**: `item_type` 파라미터의 이중 의미 충돌 해소 -2. **Material CRUD 지원**: 수정/삭제/일괄삭제/코드조회 기능 추가 - -### 1.2 현재 문제점 - -**item_type 이중 의미 충돌:** -``` -❌ 동일한 이름(item_type)이 두 가지 다른 의미로 사용됨 - -[의미 1] 품목 유형 (올바른 사용) -├─ item_pages.item_type = 'FG' | 'PT' | 'SM' | 'RM' | 'CS' -├─ common_codes (code_group='item_type') -└─ ItemsService.createItem() - product_type 파라미터 - -[의미 2] 테이블 구분 (잘못된 사용) -├─ ItemsService.getItem() - item_type = 'PRODUCT' | 'MATERIAL' -├─ ItemsController.show() - item_type 파라미터 -└─ Swagger ItemsApi - item_type 값 정의 -``` - -**Material CRUD 미지원:** - -| 메서드 | 경로 | Product | Material | 비고 | -|--------|------|:-------:|:--------:|------| -| GET | `/api/v1/items` | ✅ | ✅ | 통합 조회 정상 | -| POST | `/api/v1/items` | ✅ | ✅ | 생성 정상 | -| GET | `/api/v1/items/code/{code}` | ✅ | ❌ | Product만 지원 | -| GET | `/api/v1/items/{id}` | ✅ | ✅ | item_type으로 분기 | -| PUT | `/api/v1/items/{id}` | ✅ | ❌ | **수정 필요** | -| DELETE | `/api/v1/items/{id}` | ✅ | ❌ | **수정 필요** | -| DELETE | `/api/v1/items/batch` | ✅ | ❌ | **수정 필요** | - ---- - -## 2. 설계 결정 사항 - -### 2.1 명명 규칙 - -| 용어 | 필드명 | 값 | 설명 | -|------|--------|-----|------| -| **품목 유형** | `item_type` | FG, PT, SM, RM, CS | 비즈니스 분류 (API 파라미터) | -| **저장 테이블** | `source_table` | products, materials | 물리적 저장소 (내부 자동 매핑) | -| **참조 타입** | `ref_type` | PRODUCT, MATERIAL | 폴리모픽 관계용 (기존 유지) | - -### 2.2 품목 유형 → 테이블 매핑 - -``` -item_type → source_table 자동 매핑 - -FG (완제품) ─┐ -PT (반제품) ─┴─→ source_table = 'products' - -SM (부자재) ─┐ -RM (원자재) ─┼─→ source_table = 'materials' -CS (소모품) ─┘ -``` - -### 2.3 테넌트별 품목 유형 관리 - -- 품목 유형은 테넌트(업체)별로 커스터마이징 가능 -- **`attributes` JSON 필드 활용** (스키마 변경 없음) -- 캐시를 통한 성능 최적화 - -```json -// common_codes 레코드 예시 -{ - "code_group": "item_type", - "code": "FG", - "name": "완제품", - "attributes": { - "source_table": "products" - } -} -``` - -### 2.4 API 파라미터 변경 (Breaking Change) - -``` -Before: GET /api/v1/items/{id}?item_type=PRODUCT -After: GET /api/v1/items/{id}?item_type=FG - -Before: DELETE /api/v1/items/{id} (item_type 없음) -After: DELETE /api/v1/items/{id}?item_type=RM -``` - -### 2.5 기존 시스템 호환성 (변경 없음) - -| 시스템 | 필드 | 값 | 상태 | -|--------|------|-----|------| -| Prices API | `item_type_code` | PRODUCT, MATERIAL | ✅ 유지 | -| BOM API | `ref_type` | PRODUCT, MATERIAL | ✅ 유지 | -| ItemMaster API | `item_type` | FG, PT, SM, RM, CS | ✅ 유지 | - ---- - -## 3. 영향 범위 - -### 3.1 수정 파일 - -| 파일 | 경로 | 변경 내용 | -|------|------|----------| -| ItemTypeSeeder | `database/seeders/ItemTypeSeeder.php` | attributes.source_table 추가 | -| ItemTypeHelper | `app/Helpers/ItemTypeHelper.php` | **신규 생성** | -| ItemStoreRequest | `app/Http/Requests/Item/ItemStoreRequest.php` | Material 필드 추가 | -| ItemUpdateRequest | `app/Http/Requests/Item/ItemUpdateRequest.php` | item_type 필수화 + Material 필드 | -| ItemBatchDeleteRequest | `app/Http/Requests/Item/ItemBatchDeleteRequest.php` | item_type 필드 추가 | -| ItemsService | `app/Services/ItemsService.php` | Material CRUD 메서드 추가 | -| ItemsController | `app/Http/Controllers/Api/V1/ItemsController.php` | item_type 파라미터 처리 | -| ItemsApi | `app/Swagger/v1/ItemsApi.php` | item_type 값 변경 + 스키마 보완 | - -### 3.2 작업 완료 후 예상 상태 - -| 품목 유형 | 조회 | 생성 | 수정 | 삭제 | 일괄삭제 | 코드조회 | -|----------|:----:|:----:|:----:|:----:|:--------:|:--------:| -| 제품(FG) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| 반제품(PT) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| 부자재(SM) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| 원자재(RM) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| 소모품(CS) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | - ---- - -## 4. 작업 계획 - -### Phase 0: Seeder 수정 (5분) - -**파일**: `database/seeders/ItemTypeSeeder.php` - -**작업 내용**: -- attributes.source_table 추가 - -**수정 후 코드**: -```php -$itemTypes = [ - ['code' => 'FG', 'name' => '완제품', 'attributes' => ['source_table' => 'products']], - ['code' => 'PT', 'name' => '반제품', 'attributes' => ['source_table' => 'products']], - ['code' => 'SM', 'name' => '부자재', 'attributes' => ['source_table' => 'materials']], - ['code' => 'RM', 'name' => '원자재', 'attributes' => ['source_table' => 'materials']], - ['code' => 'CS', 'name' => '소모품', 'attributes' => ['source_table' => 'materials']], -]; -``` - -**체크리스트**: -- [x] ItemTypeSeeder.php 수정 ✅ (2025-12-09) -- [x] `php artisan db:seed --class=ItemTypeSeeder` 실행 ✅ -- [x] DB 데이터 확인 ✅ (5개 item_type 코드 시딩 완료) - ---- - -### Phase 1: ItemTypeHelper 생성 (10분) - -**파일**: `app/Helpers/ItemTypeHelper.php` (신규) - -**작업 내용**: -- 테넌트별 품목 유형 조회 (캐시 적용) -- isMaterial(), isProduct(), getSourceTable() 메서드 -- 캐시 무효화 메서드 - -**체크리스트**: -- [ ] `app/Helpers/` 디렉토리 생성 (없으면) -- [ ] ItemTypeHelper.php 생성 -- [ ] composer.json autoload 확인 (PSR-4로 자동 로드됨) - ---- - -### Phase 2: FormRequest 수정 (10분) - -**파일 3개**: - -#### 2-1. ItemStoreRequest.php -```php -// Material 전용 필드 추가 -'specification' => 'nullable|string|max:255', -'remarks' => 'nullable|string', -'item_name' => 'nullable|string|max:255', -'search_tag' => 'nullable|string|max:255', -'is_inspection' => 'nullable|string|in:Y,N', -'options' => 'nullable|array', -'material_code' => 'nullable|string|max:50', -``` - -#### 2-2. ItemUpdateRequest.php -```php -// item_type 필수화 + Material 필드 추가 -'item_type' => 'required|string|in:FG,PT,SM,RM,CS', -'material_code' => 'sometimes|string|max:50', -'specification' => 'nullable|string|max:255', -'remarks' => 'nullable|string', -'item_name' => 'nullable|string|max:255', -'search_tag' => 'nullable|string|max:255', -'is_inspection' => 'nullable|string|in:Y,N', -'options' => 'nullable|array', -``` - -#### 2-3. ItemBatchDeleteRequest.php -```php -// item_type 필드 추가 -'item_type' => 'required|string|in:FG,PT,SM,RM,CS', -``` - -**체크리스트**: -- [ ] ItemStoreRequest.php 수정 -- [ ] ItemUpdateRequest.php 수정 -- [ ] ItemBatchDeleteRequest.php 수정 - ---- - -### Phase 3: ItemsService 수정 (20분) - -**파일**: `app/Services/ItemsService.php` - -**작업 내용**: - -1. **import 추가**: `use App\Helpers\ItemTypeHelper;` - -2. **getItems() 수정**: 목록 조회 시 `specification` 추가 + `attributes` 플랫 전개 - ```php - // Product 쿼리 select에 추가 - DB::raw('NULL as specification'), // Product는 specification 컬럼 없음 - 'attributes', - - // Material 쿼리 select에 추가 - 'specification', - 'attributes', - - // 조회 후 attributes 플랫 전개 (후처리) - $items->getCollection()->transform(function ($item) { - $data = $item->toArray(); - $attributes = $data['attributes'] ?? []; - unset($data['attributes']); - return array_merge($data, $attributes); - }); - ``` - -3. **getItem() 수정**: item_type 파라미터를 FG/PT/SM/RM/CS로 받도록 변경 - ```php - // Before: getItem('PRODUCT', $id, ...) - // After: getItem($id, 'FG', ...) - ``` - -4. **updateItem() 수정**: Material 분기 추가 - ```php - public function updateItem(int $id, array $data): Product|Material - { - $itemType = strtoupper($data['item_type'] ?? 'FG'); - if (ItemTypeHelper::isMaterial($itemType, $this->tenantId())) { - return $this->updateMaterial($id, $data); - } - return $this->updateProduct($id, $data); - } - ``` - -4. **updateMaterial() 추가**: Material 수정 로직 - -5. **deleteItem() 수정**: item_type 파라미터 추가 - ```php - public function deleteItem(int $id, string $itemType = 'FG'): void - ``` - -6. **deleteMaterial() 추가**: Material 삭제 로직 - -7. **batchDeleteItems() 수정**: item_type 지원 - -8. **getItemByCode() 수정**: Product 없으면 Material에서 조회 - -9. **checkMaterialUsageInBom() 추가**: BOM 사용 여부 체크 - -**체크리스트**: -- [ ] ItemTypeHelper import 추가 -- [ ] getItems() specification/attributes 추가 + 플랫 전개 후처리 -- [ ] getItem() 시그니처 및 로직 변경 -- [ ] updateItem() Material 분기 추가 -- [ ] updateMaterial() 메서드 추가 -- [ ] deleteItem() item_type 파라미터 추가 -- [ ] deleteMaterial() 메서드 추가 -- [ ] batchDeleteItems() Material 지원 -- [ ] getItemByCode() Material 지원 -- [ ] checkMaterialUsageInBom() 메서드 추가 - ---- - -### Phase 4: ItemsController 수정 (5분) - -**파일**: `app/Http/Controllers/Api/V1/ItemsController.php` - -**작업 내용**: - -1. **show() 수정**: 기본값 'PRODUCT' → 'FG' - ```php - $itemType = strtoupper($request->input('item_type', 'FG')); - ``` - -2. **destroy() 수정**: item_type 파라미터 추가 - ```php - public function destroy(Request $request, int $id) - { - $itemType = strtoupper($request->input('item_type', 'FG')); - return ApiResponse::handle(function () use ($id, $itemType) { - $this->service->deleteItem($id, $itemType); - return 'success'; - }, __('message.item.deleted')); - } - ``` - -3. **batchDestroy() 수정**: item_type 처리 - ```php - $itemType = strtoupper($request->validated()['item_type'] ?? 'FG'); - $this->service->batchDeleteItems($request->validated()['ids'], $itemType); - ``` - -**체크리스트**: -- [ ] show() 기본값 변경 -- [ ] destroy() item_type 파라미터 추가 -- [ ] batchDestroy() item_type 처리 - ---- - -### Phase 5: Swagger 문서 수정 (15분) - -**파일**: `app/Swagger/v1/ItemsApi.php` - -**작업 내용**: - -1. **Item 스키마 보완**: - - `item_type` (FG|PT|SM|RM|CS) - 품목 유형 - - Material 전용 필드 추가 - -2. **ItemCreateRequest 스키마 보완**: - - Material 전용 필드 추가 - -3. **ItemUpdateRequest 스키마 보완**: - - `item_type` 필수 (FG|PT|SM|RM|CS) - - Material 전용 필드 추가 - -4. **ItemBatchDeleteRequest 스키마 보완**: - - `item_type` 필드 추가 - -5. **show 엔드포인트**: - - item_type 값 변경 (common_codes): PRODUCT|MATERIAL → FG|PT|SM|RM|CS - -6. **destroy 엔드포인트**: - - item_type 쿼리 파라미터 추가 - -7. **batchDestroy 엔드포인트**: - - Request에 item_type 필드 추가 - -8. **showByCode 엔드포인트**: - - Product/Material 모두 지원 설명 추가 - -**체크리스트**: -- [ ] Item 스키마 보완 -- [ ] ItemCreateRequest 스키마 보완 -- [ ] ItemUpdateRequest 스키마 보완 -- [ ] ItemBatchDeleteRequest 스키마 보완 -- [ ] show 엔드포인트 item_type 값 변경 -- [ ] destroy 엔드포인트 item_type 파라미터 추가 -- [ ] batchDestroy 엔드포인트 스키마 수정 -- [ ] showByCode 엔드포인트 설명 보완 - ---- - -### Phase 6: 검증 (10분) - -**작업 내용**: - -1. **Pint 코드 포맷팅** - ```bash - cd api && ./vendor/bin/pint - ``` - -2. **Swagger 생성 테스트** - ```bash - cd api && php artisan l5-swagger:generate - ``` - -3. **API 테스트** (Swagger UI) - - Product CRUD 정상 동작 확인 - - Material CRUD 정상 동작 확인 - - 코드 중복 체크 동작 확인 - - BOM 사용 중 삭제 방지 확인 - -**체크리스트**: -- [ ] Pint 실행 및 오류 수정 -- [ ] Swagger 생성 성공 -- [ ] Product CRUD 테스트 -- [ ] Material CRUD 테스트 - ---- - -## 5. 예상 시간 - -| Phase | 작업 | 예상 시간 | -|-------|------|----------| -| 0 | Seeder 수정 | 5분 | -| 1 | ItemTypeHelper 생성 | 10분 | -| 2 | FormRequest 수정 (3개) | 10분 | -| 3 | ItemsService 수정 | 20분 | -| 4 | ItemsController 수정 | 5분 | -| 5 | Swagger 문서 수정 | 15분 | -| 6 | 검증 | 10분 | -| **총계** | | **약 75분** | - ---- - -## 6. 아키텍처 다이어그램 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ API Request │ -│ item_type = 'FG' | 'SM' | ... │ -└─────────────────────────┬───────────────────────────────────┘ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ ItemTypeHelper │ -│ ┌─────────────────────────────────────────────────────┐ │ -│ │ getTypesByTenant($tenantId) │ │ -│ │ → Cache::remember("item_types:tenant:{id}") │ │ -│ │ → CommonCode::where('code_group', 'item_type') │ │ -│ └─────────────────────────────────────────────────────┘ │ -│ ┌─────────────────────────────────────────────────────┐ │ -│ │ isMaterial($itemType, $tenantId) │ │ -│ │ isProduct($itemType, $tenantId) │ │ -│ │ getSourceTable($itemType, $tenantId) │ │ -│ └─────────────────────────────────────────────────────┘ │ -└─────────────────────────┬───────────────────────────────────┘ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ ItemsService │ -│ if (ItemTypeHelper::isMaterial($itemType, $tenantId)) { │ -│ → materials 테이블 │ -│ } else { │ -│ → products 테이블 │ -│ } │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## 7. 변경 이력 - -| 날짜 | 내용 | -|------|------| -| 2025-12-09 | 통합 문서 작성 (items-naming-convention.md + items-api-modification-plan.md 병합) | - ---- - -## 8. API별 Request/Response 예시 - -### 8.1 품목 목록 조회 (GET /api/v1/items) - -> 🔧 **변경**: `specification` 추가 + `attributes` 플랫 전개 + 페이지네이션 구조 변경 - -**Request:** -```http -GET /api/v1/items?type=FG,PT&search=스크린&page=1&size=20 -``` - -**Response:** -```json -{ - "success": true, - "message": "조회되었습니다.", - "data": [ - { - "id": 1, - "item_type": "FG", - "code": "P-001", - "name": "스크린 제품 A", - "specification": null, - "unit": "EA", - "category_id": 1, - "color": "white", - "size": "100x200", - "created_at": "2025-12-01 10:00:00", - "deleted_at": null - }, - { - "id": 5, - "item_type": "RM", - "code": "M-001", - "name": "스크린 원단", - "specification": "1.2T x 1219 x 2438", - "unit": "M", - "category_id": 2, - "thickness": "1.2T", - "width": "1219", - "created_at": "2025-12-01 11:00:00", - "deleted_at": null - } - ], - "pagination": { - "current_page": 1, - "per_page": 20, - "total": 2, - "last_page": 1, - "from": 1, - "to": 2 - } -} -``` - -**응답 구조:** - -| 키 | 타입 | 설명 | -|----|------|------| -| `success` | boolean | 성공 여부 | -| `message` | string | 메시지 | -| `data` | array | **품목 데이터 배열** (바로 접근 가능) | -| `pagination` | object | **페이지네이션 정보** | - -**data[] 필드 설명:** - -| 필드 | Product | Material | 설명 | -|------|---------|----------|------| -| `id` | ✅ | ✅ | 품목 ID | -| `item_type` | FG/PT | SM/RM/CS | 품목 유형 | -| `code` | ✅ | ✅ | 품목 코드 | -| `name` | ✅ | ✅ | 품목명 | -| `specification` | null | ✅ | 규격 (Material만 컬럼 존재) | -| `unit` | ✅ | ✅ | 단위 | -| `category_id` | ✅ | ✅ | 카테고리 ID | -| `{attr_key}` | ✅ | ✅ | **attributes JSON 플랫 전개** (동적 필드) | -| `created_at` | ✅ | ✅ | 생성일 | -| `deleted_at` | ✅ | ✅ | 삭제일 (soft delete) | - -**pagination 필드 설명:** - -| 필드 | 타입 | 설명 | -|------|------|------| -| `current_page` | int | 현재 페이지 | -| `per_page` | int | 페이지당 항목 수 | -| `total` | int | 전체 항목 수 | -| `last_page` | int | 마지막 페이지 | -| `from` | int | 현재 페이지 시작 번호 | -| `to` | int | 현재 페이지 끝 번호 | - -**⚠️ 구현 (ItemsService.getItems()):** -```php -// 1. attributes 플랫 전개 -$items->getCollection()->transform(function ($item) { - $data = $item->toArray(); - $attributes = $data['attributes'] ?? []; - unset($data['attributes']); - return array_merge($data, $attributes); -}); - -// 2. 페이지네이션 구조 변환 (Controller에서) -return [ - 'data' => $items->items(), - 'pagination' => [ - 'current_page' => $items->currentPage(), - 'per_page' => $items->perPage(), - 'total' => $items->total(), - 'last_page' => $items->lastPage(), - 'from' => $items->firstItem(), - 'to' => $items->lastItem(), - ], -]; -``` - ---- - -### 8.2 품목 생성 (POST /api/v1/items) - -> 🔧 **변경**: Request/Response 모두 플랫 구조 (품목기준관리 필드 기반) - -#### Product 생성 (FG/PT) - -**Request:** -```json -{ - "code": "P-002", - "name": "스크린 제품 B", - "product_type": "FG", - "unit": "EA", - "category_id": 1, - "description": "완제품 설명", - "is_sellable": true, - "is_purchasable": false, - "is_producible": true, - "safety_stock": 10, - "lead_time": 7, - "color": "white", - "size": "100x200" -} -``` - -> **참고**: 모든 필드는 플랫하게 전송. 백엔드에서 품목기준관리(item_fields) 정의에 따라 고정 컬럼과 attributes JSON 저장을 자동 분리. - -**Response:** -```json -{ - "success": true, - "message": "품목이 등록되었습니다.", - "data": { - "id": 2, - "tenant_id": 1, - "code": "P-002", - "name": "스크린 제품 B", - "product_type": "FG", - "unit": "EA", - "category_id": 1, - "description": "완제품 설명", - "is_sellable": true, - "is_purchasable": false, - "is_producible": true, - "is_active": true, - "safety_stock": 10, - "lead_time": 7, - "color": "white", - "size": "100x200", - "created_at": "2025-12-09 10:00:00", - "updated_at": "2025-12-09 10:00:00" - } -} -``` - -#### Material 생성 (SM/RM/CS) - -**Request:** -```json -{ - "code": "M-002", - "name": "철판 1.2T", - "product_type": "RM", - "unit": "EA", - "category_id": 2, - "item_name": "철판", - "specification": "1.2T x 1219 x 2438", - "is_inspection": "Y", - "search_tag": "철판,원자재,1.2T", - "remarks": "포스코산", - "thickness": "1.2T", - "width": "1219" -} -``` - -**Response:** -```json -{ - "success": true, - "message": "품목이 등록되었습니다.", - "data": { - "id": 10, - "tenant_id": 1, - "material_code": "M-002", - "name": "철판 1.2T", - "material_type": "RM", - "unit": "EA", - "category_id": 2, - "item_name": "철판", - "specification": "1.2T x 1219 x 2438", - "is_inspection": "Y", - "search_tag": "철판,원자재,1.2T", - "remarks": "포스코산", - "thickness": "1.2T", - "width": "1219", - "is_active": true, - "created_at": "2025-12-09 10:05:00", - "updated_at": "2025-12-09 10:05:00" - } -} -``` - ---- - -### 8.3 품목 상세 조회 - ID (GET /api/v1/items/{id}) - -> 🔧 **변경**: `item_type` 파라미터 값 변경 + `attributes` 플랫 전개 - -#### Before (현재) -```http -GET /api/v1/items/1?item_type=PRODUCT&include_price=true -GET /api/v1/items/10?item_type=MATERIAL -``` - -#### After (변경 후) -```http -GET /api/v1/items/1?item_type=FG&include_price=true -GET /api/v1/items/10?item_type=RM -``` - -**Response (Product):** -```json -{ - "success": true, - "message": "조회되었습니다.", - "data": { - "id": 1, - "item_type": "FG", - "code": "P-001", - "name": "스크린 제품 A", - "unit": "EA", - "category_id": 1, - "category": { - "id": 1, - "name": "완제품" - }, - "description": "완제품 설명", - "is_sellable": true, - "is_purchasable": false, - "is_producible": true, - "is_active": true, - "safety_stock": 10, - "lead_time": 7, - "color": "white", - "size": "100x200", - "prices": { - "sale": { - "price": 150000, - "currency": "KRW", - "effective_from": "2025-01-01" - }, - "purchase": null - }, - "created_at": "2025-12-01 10:00:00", - "updated_at": "2025-12-01 10:00:00" - } -} -``` - -**Response (Material):** -```json -{ - "success": true, - "message": "조회되었습니다.", - "data": { - "id": 10, - "item_type": "RM", - "code": "M-002", - "name": "철판 1.2T", - "unit": "EA", - "category_id": 2, - "item_name": "철판", - "specification": "1.2T x 1219 x 2438", - "is_inspection": "Y", - "search_tag": "철판,원자재,1.2T", - "remarks": "포스코산", - "thickness": "1.2T", - "width": "1219", - "is_active": true, - "created_at": "2025-12-09 10:05:00", - "updated_at": "2025-12-09 10:05:00" - } -} -``` - ---- - -### 8.4 품목 상세 조회 - 코드 (GET /api/v1/items/code/{code}) - -> 🔧 **변경**: Material 코드 조회 지원 추가 + `attributes` 플랫 전개 - -#### Before (현재) -```http -GET /api/v1/items/code/P-001?include_bom=true # ✅ Product만 지원 -GET /api/v1/items/code/M-002 # ❌ 404 에러 -``` - -#### After (변경 후) -```http -GET /api/v1/items/code/P-001?include_bom=true # ✅ Product 조회 -GET /api/v1/items/code/M-002 # ✅ Material 조회 -``` - -**Response (Product):** -```json -{ - "success": true, - "message": "품목을 조회했습니다.", - "data": { - "id": 1, - "item_type": "FG", - "code": "P-001", - "name": "스크린 제품 A", - "unit": "EA", - "category_id": 1, - "category": { - "id": 1, - "name": "완제품" - }, - "description": "완제품 설명", - "is_sellable": true, - "is_purchasable": false, - "is_producible": true, - "is_active": true, - "safety_stock": 10, - "lead_time": 7, - "color": "white", - "size": "100x200", - "component_lines": [ - { - "id": 1, - "child_product": { - "id": 2, - "code": "PT-001", - "name": "스크린 본체", - "unit": "EA" - }, - "quantity": 1 - } - ], - "created_at": "2025-12-01 10:00:00", - "updated_at": "2025-12-01 10:00:00" - } -} -``` - -**Response (Material):** -```json -{ - "success": true, - "message": "품목을 조회했습니다.", - "data": { - "id": 10, - "item_type": "RM", - "code": "M-002", - "name": "철판 1.2T", - "unit": "EA", - "category_id": 2, - "item_name": "철판", - "specification": "1.2T x 1219 x 2438", - "is_inspection": "Y", - "search_tag": "철판,원자재,1.2T", - "remarks": "포스코산", - "thickness": "1.2T", - "width": "1219", - "is_active": true, - "created_at": "2025-12-09 10:05:00", - "updated_at": "2025-12-09 10:05:00" - } -} -``` - ---- - -### 8.5 품목 수정 (PUT /api/v1/items/{id}) - -> 🔧 **변경**: `item_type` 필수화 + Material 수정 지원 + `attributes` 플랫 전개 - -#### Before (현재) -```http -PUT /api/v1/items/1 -Content-Type: application/json - -{ - "name": "스크린 제품 A (수정)" # ✅ Product만 지원 -} -``` - -#### After (변경 후) - -**Product 수정:** -```http -PUT /api/v1/items/1 -Content-Type: application/json - -{ - "item_type": "FG", - "name": "스크린 제품 A (수정)", - "description": "수정된 설명", - "safety_stock": 20, - "color": "black", - "size": "150x300" -} -``` - -> **참고**: 모든 필드는 플랫하게 전송. 백엔드에서 품목기준관리(item_fields) 정의에 따라 고정 컬럼과 attributes JSON 저장을 자동 분리. - -**Response (Product):** -```json -{ - "success": true, - "message": "품목이 수정되었습니다.", - "data": { - "id": 1, - "item_type": "FG", - "code": "P-001", - "name": "스크린 제품 A (수정)", - "unit": "EA", - "category_id": 1, - "description": "수정된 설명", - "is_sellable": true, - "is_purchasable": false, - "is_producible": true, - "is_active": true, - "safety_stock": 20, - "lead_time": 7, - "color": "black", - "size": "150x300", - "created_at": "2025-12-01 10:00:00", - "updated_at": "2025-12-09 11:00:00" - } -} -``` - -**Material 수정:** -```http -PUT /api/v1/items/10 -Content-Type: application/json - -{ - "item_type": "RM", - "name": "철판 1.5T", - "specification": "1.5T x 1219 x 2438", - "remarks": "포스코산 (규격 변경)", - "thickness": "1.5T", - "width": "1219" -} -``` - -**Response (Material):** -```json -{ - "success": true, - "message": "품목이 수정되었습니다.", - "data": { - "id": 10, - "item_type": "RM", - "code": "M-002", - "name": "철판 1.5T", - "unit": "EA", - "category_id": 2, - "item_name": "철판", - "specification": "1.5T x 1219 x 2438", - "is_inspection": "Y", - "search_tag": "철판,원자재,1.5T", - "remarks": "포스코산 (규격 변경)", - "thickness": "1.5T", - "width": "1219", - "is_active": true, - "created_at": "2025-12-09 10:05:00", - "updated_at": "2025-12-09 11:05:00" - } -} -``` - ---- - -### 8.6 품목 삭제 (DELETE /api/v1/items/{id}) - -> 🔧 **변경**: `item_type` 쿼리 파라미터 추가 + Material 삭제 지원 - -#### Before (현재) -```http -DELETE /api/v1/items/1 # ✅ Product만 삭제 -DELETE /api/v1/items/10 # ❌ Material은 404 에러 -``` - -#### After (변경 후) -```http -DELETE /api/v1/items/1?item_type=FG # ✅ Product 삭제 -DELETE /api/v1/items/10?item_type=RM # ✅ Material 삭제 -``` - -**Response (성공):** -```json -{ - "success": true, - "message": "품목이 삭제되었습니다.", - "data": "success" -} -``` - -**Response (BOM 사용 중 - 삭제 불가):** -```json -{ - "success": false, - "message": "다른 BOM의 구성품으로 사용 중입니다. (3건)", - "error": { - "code": "ITEM_IN_USE", - "count": 3 - } -} -``` - ---- - -### 8.7 품목 일괄 삭제 (DELETE /api/v1/items/batch) - -> 🔧 **변경**: `item_type` 필드 추가 + Material 일괄 삭제 지원 - -#### Before (현재) -```http -DELETE /api/v1/items/batch -Content-Type: application/json - -{ - "ids": [1, 2, 3] # ✅ Product만 삭제 -} -``` - -#### After (변경 후) - -**Product 일괄 삭제:** -```http -DELETE /api/v1/items/batch -Content-Type: application/json - -{ - "item_type": "FG", - "ids": [1, 2, 3] -} -``` - -**Material 일괄 삭제:** -```http -DELETE /api/v1/items/batch -Content-Type: application/json - -{ - "item_type": "RM", - "ids": [10, 11, 12] -} -``` - -**Response (성공):** -```json -{ - "success": true, - "message": "품목이 일괄 삭제되었습니다.", - "data": "success" -} -``` - -**Response (일부 BOM 사용 중):** -```json -{ - "success": false, - "message": "일부 품목이 BOM 구성품으로 사용 중입니다. (2건)", - "error": { - "code": "ITEMS_IN_USE", - "count": 2 - } -} -``` - ---- - -### 8.8 에러 응답 예시 - -**404 Not Found:** -```json -{ - "success": false, - "message": "품목을 찾을 수 없습니다.", - "error": { - "code": "NOT_FOUND" - } -} -``` - -**400 Bad Request (코드 중복):** -```json -{ - "success": false, - "message": "이미 사용 중인 품목코드입니다.", - "error": { - "code": "DUPLICATE_CODE" - } -} -``` - -**422 Validation Error:** -```json -{ - "success": false, - "message": "입력값이 올바르지 않습니다.", - "errors": { - "item_type": ["품목 유형은 필수입니다."], - "name": ["품목명은 255자 이내로 입력하세요."] - } -} -``` - ---- - -## 9. 관련 문서 (삭제 완료) - -아래 문서들은 이 통합 문서로 병합되어 삭제되었습니다: -- ~~`docs/plans/items-naming-convention.md`~~ -- ~~`docs/plans/items-api-modification-plan.md`~~ \ No newline at end of file diff --git a/rules/README.md b/rules/README.md index 9754057..7252b66 100644 --- a/rules/README.md +++ b/rules/README.md @@ -18,6 +18,7 @@ | 문서 | 설명 | |------|------| +| [item-policy.md](item-policy.md) | 품목 정책 (유형 체계, 예약어, API 규칙) | | [pricing-policy.md](pricing-policy.md) | 단가 정책 (원가/판매가 계산, 리비전 관리) | ## 관련 폴더 diff --git a/rules/item-policy.md b/rules/item-policy.md new file mode 100644 index 0000000..161fe6b --- /dev/null +++ b/rules/item-policy.md @@ -0,0 +1,293 @@ +# 품목(Items) 비즈니스 정책 + +> 품목 관리 시스템의 핵심 비즈니스 규칙 정의 +> +> **최종 업데이트**: 2025-12-09 + +--- + +## 1. 품목 유형 체계 (item_type) + +### 1.1 품목 유형 코드 정의 + +| 코드 | 한글명 | 영문명 | source_table | 설명 | +|------|--------|--------|--------------|------| +| `FG` | 완제품 | Finished Goods | products | 판매 가능한 최종 제품 | +| `PT` | 부품 | Parts | products | 제품 구성에 사용되는 부품 | +| `SM` | 부자재 | Sub-Materials | materials | 생산에 사용되는 보조 자재 | +| `RM` | 원자재 | Raw Materials | materials | 생산의 주요 원료 | +| `CS` | 소모품 | Consumables | materials | 일회성 소모 자재 | + +### 1.2 저장 위치 + +``` +common_codes 테이블 +├─ code_group = 'item_type' +├─ code = 'FG' | 'PT' | 'SM' | 'RM' | 'CS' +└─ attributes.source_table = 'products' | 'materials' +``` + +### 1.3 source_table 매핑 규칙 + +``` +item_type → source_table 자동 매핑: + +FG (완제품) ─┐ +PT (부품) ─┴─→ products 테이블 + +SM (부자재) ─┐ +RM (원자재) ─┼─→ materials 테이블 +CS (소모품) ─┘ +``` + +### 1.4 테넌트별 확장 + +- 품목 유형은 테넌트별로 커스터마이징 가능 +- `common_codes.attributes` JSON 필드 활용 (스키마 변경 없음) +- 새 품목 유형 추가 시 source_table 매핑 필수 + +--- + +## 2. 용어 정의 및 구분 + +### 2.1 핵심 용어 + +| 용어 | 필드명 | 값 | 용도 | +|------|--------|-----|------| +| **품목 유형** | `item_type` | FG, PT, SM, RM, CS | API 파라미터, UI 필터링, 비즈니스 분류 | +| **저장 테이블** | `source_table` | products, materials | 내부 DB 분기, 서비스 로직 | +| **참조 타입** | `ref_type` | PRODUCT, MATERIAL | 폴리모픽 관계 (BOM, Prices 등) | + +### 2.2 사용 규칙 + +- **API 레벨**: `item_type` 파라미터 사용 +- **서비스 레벨**: `source_table`로 테이블 분기 +- **폴리모픽 관계**: `ref_type`으로 참조 (기존 호환성 유지) + +### 2.3 API 흐름 예시 + +``` +1. 클라이언트 요청: GET /api/v1/items?item_type=FG + ↓ +2. 서버 처리: item_type='FG' → common_codes 조회 + ↓ +3. 테이블 분기: source_table='products' 확인 + ↓ +4. 데이터 조회: products 테이블에서 조회 + ↓ +5. 응답 반환: item_type='FG' 포함하여 응답 +``` + +--- + +## 3. 필드 키 예약어 정책 + +### 3.1 products 테이블 예약어 + +```php +// 사용 불가 field_key 목록 +'code', 'name', 'unit', 'category_id', 'product_type', 'description', +'is_sellable', 'is_purchasable', 'is_producible', 'is_variable_size', 'is_active', +'safety_stock', 'lead_time', 'product_category', 'part_type', +'bending_diagram', 'bending_details', +'specification_file', 'specification_file_name', +'certification_file', 'certification_file_name', +'certification_number', 'certification_start_date', 'certification_end_date', +'attributes', 'attributes_archive' +``` + +### 3.2 materials 테이블 예약어 + +```php +// 사용 불가 field_key 목록 +'name', 'item_name', 'specification', 'material_code', 'material_type', +'unit', 'category_id', 'is_inspection', 'is_active', +'search_tag', 'remarks', 'attributes', 'options' +``` + +### 3.3 공통 시스템 컬럼 (모든 테이블) + +```php +// 절대 사용 불가 +'id', 'tenant_id', 'created_by', 'updated_by', 'deleted_by', +'created_at', 'updated_at', 'deleted_at' +``` + +### 3.4 검증 규칙 + +1. **field_key 저장**: 입력값 그대로 저장 (id 접두사 없음) +2. **예약어 검증 흐름**: + ``` + field_key 입력 + ↓ + source_table 확인 (products / materials) + ↓ + 해당 테이블 예약어 체크 + ↓ + 기존 필드 중복 체크 + ↓ + 저장 + ``` + +3. **에러 메시지**: + - 예약어 충돌: `"{field_key}"은(는) 시스템 예약어로 사용할 수 없습니다.` + - 중복: `field_key은(는) 이미 사용 중입니다.` + +--- + +## 4. API 파라미터 규칙 + +### 4.1 목록 조회 (GET /api/v1/items) + +| 파라미터 | 필수 | 값 | 설명 | +|---------|:----:|-----|------| +| `type` | ❌ | FG,PT,SM,RM,CS | 품목 유형 필터 (쉼표 구분) | +| `search` | ❌ | string | 코드/명칭 검색 | +| `page` | ❌ | int | 페이지 번호 | +| `size` | ❌ | int | 페이지 크기 | + +### 4.2 단건 조회 (GET /api/v1/items/{id}) + +| 파라미터 | 필수 | 값 | 설명 | +|---------|:----:|-----|------| +| `item_type` | ✅ | FG/PT/SM/RM/CS | 품목 유형 (테이블 분기용) | +| `include_price` | ❌ | boolean | 단가 정보 포함 | + +### 4.3 생성 (POST /api/v1/items) + +| 파라미터 | 필수 | 값 | 설명 | +|---------|:----:|-----|------| +| `product_type` | ✅ | FG/PT/SM/RM/CS | 품목 유형 | +| `code` | ✅ | string | 품목 코드 | +| `name` | ✅ | string | 품목명 | +| `unit` | ✅ | string | 단위 | + +### 4.4 수정 (PUT /api/v1/items/{id}) + +| 파라미터 | 필수 | 값 | 설명 | +|---------|:----:|-----|------| +| `item_type` | ✅ | FG/PT/SM/RM/CS | 품목 유형 (테이블 분기용) | + +### 4.5 삭제 (DELETE /api/v1/items/{id}) + +| 파라미터 | 필수 | 값 | 설명 | +|---------|:----:|-----|------| +| `item_type` | ✅ | FG/PT/SM/RM/CS | 품목 유형 (테이블 분기용) | + +### 4.6 일괄 삭제 (DELETE /api/v1/items/batch) + +| 파라미터 | 필수 | 값 | 설명 | +|---------|:----:|-----|------| +| `item_type` | ✅ | FG/PT/SM/RM/CS | 품목 유형 | +| `ids` | ✅ | array | 삭제할 ID 목록 | + +--- + +## 5. 데이터 저장 규칙 + +### 5.1 저장 방식 구분 + +| 저장 방식 | 대상 | 설명 | +|----------|------|------| +| **column** | 고정 컬럼 | DB 테이블 컬럼에 직접 저장 | +| **json** | 동적 속성 | `attributes` JSON 필드에 저장 | + +### 5.2 고정 컬럼 (column) + +- DB 스키마에 정의된 컬럼 +- 필드 매핑: `item_fields.source_column` 사용 +- 예: code, name, unit, category_id 등 + +### 5.3 동적 속성 (json) + +- `attributes` JSON 필드에 저장 +- 필드 매핑: `item_fields.json_path` 사용 +- 예: `attributes.custom_size`, `attributes.color` 등 + +### 5.4 API 응답 규칙 + +- **플랫 구조**: attributes 내부 값을 최상위로 전개 +- **매핑 정보 미노출**: source_table, source_column 등 내부 필드 숨김 + +```json +// 응답 예시 +{ + "id": 1, + "item_type": "FG", + "code": "P-001", + "name": "스크린 제품", + "color": "white", // attributes.color → 플랫 전개 + "size": "100x200" // attributes.size → 플랫 전개 +} +``` + +--- + +## 6. 삭제 규칙 + +### 6.1 BOM 사용 체크 + +- 품목이 다른 BOM의 구성품으로 사용 중이면 삭제 불가 +- 에러 메시지: `다른 BOM의 구성품으로 사용 중입니다. (N건)` + +### 6.2 Soft Delete + +- 모든 품목은 soft delete 방식 사용 +- `deleted_at` 타임스탬프로 삭제 표시 +- 복구 가능 + +--- + +## 7. 관련 파일 + +### 7.1 API 파일 + +| 파일 | 경로 | 역할 | +|------|------|------| +| ItemsController | `api/app/Http/Controllers/Api/V1/ItemsController.php` | API 컨트롤러 | +| ItemsService | `api/app/Services/ItemsService.php` | 비즈니스 로직 | +| ItemTypeHelper | `api/app/Helpers/ItemTypeHelper.php` | item_type 헬퍼 | +| SystemFields | `api/app/Constants/SystemFields.php` | 예약어 상수 | + +### 7.2 Seeder 파일 + +| 파일 | 경로 | 역할 | +|------|------|------| +| ItemTypeSeeder | `api/database/seeders/ItemTypeSeeder.php` | item_type 코드 시딩 | + +### 7.3 Request 파일 + +| 파일 | 경로 | +|------|------| +| ItemStoreRequest | `api/app/Http/Requests/Item/ItemStoreRequest.php` | +| ItemUpdateRequest | `api/app/Http/Requests/Item/ItemUpdateRequest.php` | +| ItemBatchDeleteRequest | `api/app/Http/Requests/Item/ItemBatchDeleteRequest.php` | + +--- + +## 8. 구현 현황 + +### 8.1 완료 항목 + +- ✅ item_type 코드 시딩 (ItemTypeSeeder) +- ✅ common_codes에 attributes.source_table 매핑 +- ✅ field_key 예약어 검증 (SystemFields) +- ✅ ItemMaster CRUD API (Pages, Sections, Fields) +- ✅ 독립 엔티티 아키텍처 (entity_relationships) + +### 8.2 개발 필요 항목 + +| API | Product | Material | 작업 내용 | +|-----|:-------:|:--------:|----------| +| `PUT /items/{id}` | ✅ | ❌ | Material 수정 지원 추가 | +| `DELETE /items/{id}` | ✅ | ❌ | Material 삭제 지원 추가 | +| `DELETE /items/batch` | ✅ | ❌ | Material 일괄삭제 지원 | +| `GET /items/code/{code}` | ✅ | ❌ | Material 코드 조회 지원 | + +--- + +## 변경 이력 + +| 날짜 | 내용 | +|------|------| +| 2025-12-09 | 문서 생성 - 4개 문서 통합 (items-api-unified-plan, field-integration, field-key-validation, INDEX) | \ No newline at end of file diff --git a/specs/ITEM-MASTER-INDEX.md b/specs/ITEM-MASTER-INDEX.md deleted file mode 100644 index e817a34..0000000 --- a/specs/ITEM-MASTER-INDEX.md +++ /dev/null @@ -1,225 +0,0 @@ -# Item Master 문서 인덱스 - -> 품목기준관리(ItemMaster) 관련 문서 현황 및 개발 상태 -> -> **최종 업데이트**: 2025-12-09 - ---- - -## 🔑 핵심 개념 정의 - -### item_type (품목 유형 코드) - -| 코드 | 한글명 | 영문명 | source_table | -|------|--------|--------|--------------| -| `FG` | 제품 | Finished Goods | products | -| `PT` | 부품 | Parts | products | -| `SM` | 부자재 | Sub-Materials | materials | -| `RM` | 원자재 | Raw Materials | materials | -| `CS` | 소모품 | Consumables | materials | - -> **저장 위치**: `common_codes` 테이블 (`code_group = 'item_type'`) -> **소스 테이블 매핑**: `attributes.source_table` JSON 필드 -> **시딩 상태**: ✅ 구현 완료 (`ItemTypeSeeder.php`) - -### 관련 용어 구분 - -| 용어 | 역할 | 값 | 사용처 | -|------|------|-----|--------| -| `item_type` | 품목 유형 코드 | FG/PT/SM/RM/CS | API 파라미터, 필터링, UI 표시 | -| `source_table` | 물리적 저장 테이블 | products/materials | DB 조회, 서비스 로직 분기 | -| `ref_type` | 폴리모픽 참조 타입 | PRODUCT/MATERIAL | 기존 폴리모픽 관계 유지 | - -### 매핑 규칙 - -``` -item_type → source_table 자동 매핑: -├─ FG, PT → products 테이블 -└─ SM, RM, CS → materials 테이블 - -API 흐름: -1. 클라이언트: item_type=FG 전송 -2. 서버: common_codes에서 source_table 조회 -3. 서버: products 또는 materials 테이블에 저장/조회 -``` - ---- - -## 📐 문서 관계도 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Item Master 문서 체계 │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────┐ 참조 ┌─────────────────────────────┐│ -│ │ API 가이드 │◄──────────│ items-api-unified-plan.md ││ -│ │ (front/) │ │ (plans/) ││ -│ │ │ │ - API 구현 계획 ││ -│ │ 프론트엔드용 │ │ - item_type 값 변경 스펙 ││ -│ │ API 명세 │ │ - Swagger 스키마 정의 ││ -│ └────────┬────────┘ └──────────────┬──────────────┘│ -│ │ │ │ -│ │ 연동 │ 구현 기반 │ -│ ▼ ▼ │ -│ ┌─────────────────┐ ┌─────────────────────────────┐│ -│ │ field-integration│ │ field-key-validation.md ││ -│ │ (specs/) │◄───────────│ (specs/) ││ -│ │ │ 검증 정책 │ ││ -│ │ - 필드 통합 설계 │ │ - SystemFields 상수 ││ -│ │ - source_table │ │ - 예약어 검증 로직 ││ -│ │ 기반 분기 │ │ - 에러 메시지 정의 ││ -│ └─────────────────┘ └─────────────────────────────┘│ -│ │ -│ ════════════════════════════════════════════════════════════ │ -│ 공통 기반: common_codes 테이블 (code_group='item_type') │ -│ ════════════════════════════════════════════════════════════ │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 📋 문서 구조 - -``` -docs/ -├── front/ -│ └── item-master-guide.md ✅ 최신 API 가이드 -├── specs/ -│ ├── ITEM-MASTER-INDEX.md 📋 이 파일 -│ ├── item-master-integration.md 🔧 연동 설계서 (개발 중) -│ ├── item-master-field-integration.md 📄 필드 통합 스펙 -│ └── item-master-field-key-validation.md ✅ 검증 정책 (구현 완료) -├── plans/ -│ ├── items-api-unified-plan.md 📄 Items API 통합 계획 (최신) -│ ├── items-api-modification-plan.md 📦 API 수정 계획 (통합됨) -│ ├── items-naming-convention.md 📄 명명 규칙 -│ └── flow-tests/ -│ └── item-master-*.json 🧪 API 테스트 플로우 -├── guides/ -│ └── item-management-migration.md 📄 마이그레이션 가이드 -├── data/analysis/ -│ └── item-db-analysis.md 📊 DB 분석 -└── history/2025-11/ - ├── item-master-spec.md 📦 구버전 스펙 - ├── item-master-gap-analysis.md 📦 갭 분석 - ├── front-requests/ 📦 프론트 요청서 아카이브 - └── item-master-archived/ 📦 기타 아카이브 -``` - ---- - -## 🎯 핵심 문서 (현재 유효) - -| 문서 | 경로 | 역할 | 상태 | -|------|------|------|------| -| **API 가이드** | `front/item-master-guide.md` | 프론트엔드용 API 명세 | ✅ 최신 | -| **API 통합 계획** | `plans/items-api-unified-plan.md` | Items API 통합 구현 계획 | 📄 최신 | -| **필드 통합 스펙** | `specs/item-master-field-integration.md` | source_table 기반 필드 설계 | 🔧 v1.3 구현 중 | -| **검증 정책** | `specs/item-master-field-key-validation.md` | field_key 검증 로직 | ✅ 구현 완료 | -| **연동 설계서** | `specs/item-master-integration.md` | BE-FE 연동 아키텍처 | 🔧 개발 중 | - ---- - -## 🔴 개발 필요 항목 - -### 1. Items API Material 지원 (items-api-modification-plan.md) - -| API | Product | Material | 작업 내용 | -|-----|:-------:|:--------:|----------| -| `PUT /items/{id}` | ✅ | ❌ | Material 수정 지원 추가 | -| `DELETE /items/{id}` | ✅ | ❌ | Material 삭제 지원 추가 | -| `DELETE /items/batch` | ✅ | ❌ | Material 일괄삭제 지원 | -| `GET /items/code/{code}` | ✅ | ❌ | Material 코드 조회 지원 | - -**관련 파일**: -- `api/app/Services/ItemsService.php` -- `api/app/Http/Controllers/Api/V1/ItemsController.php` - -### 2. 연동 설계 구현 (item-master-integration.md) - -| 항목 | 상태 | 설명 | -|------|------|------| -| ItemFieldValidationService | 🔧 개발 중 | attributes 값 검증 서비스 | -| Products/Materials 수정 | ⏳ 대기 | 검증 연동 적용 | -| field_meta 응답 옵션 | ⏳ 대기 | `?include_field_meta=true` | - ---- - -## ✅ 구현 완료 항목 - -### 1. field_key 검증 정책 (item-master-field-key-validation.md) - -- ✅ `SystemFields` 상수 클래스 생성 -- ✅ 시스템 예약어 검증 로직 -- ✅ source_table 기반 분기 처리 -- ✅ 에러 메시지 (`error.field_key_reserved`) - -### 2. ItemMaster CRUD API - -- ✅ Pages, Sections, Fields, BomItems CRUD -- ✅ 독립 엔티티 아키텍처 -- ✅ entity_relationships 링크 테이블 -- ✅ Lock 기능 (연결 잠금) - -### 3. item_type 코드 시딩 (ItemTypeSeeder) - -- ✅ common_codes 테이블에 item_type 그룹 데이터 시딩 -- ✅ attributes.source_table JSON 매핑 (products/materials) -- ✅ 5개 코드: FG(완제품), PT(부품), SM(부자재), RM(원자재), CS(소모품) - -### 4. DB 구조 확정 (item_pages, item_sections, item_fields) - -- ✅ item_pages 테이블 (페이지 설정) - 이미 존재, source_table 컬럼 포함 -- ✅ item_sections 테이블 (섹션 설정) - 이미 존재 -- ✅ item_fields 테이블 (필드 설정) - 이미 존재 -- ✅ entity_relationships 링크 테이블로 page→section→field 연결 - -> **중요**: item_pages는 common_codes로 대체 불가 -> - common_codes: item_type 마스터 코드 정의 (FG/PT/SM/RM/CS) -> - item_pages: UI 페이지 설정 (같은 item_type에 여러 페이지 가능) -> - 두 테이블은 역할이 다르며 공존해야 함 - ---- - -## 📦 아카이브 (History) - -구버전 문서는 `docs/history/2025-11/`에 보관됨: - -| 폴더 | 내용 | -|------|------| -| `front-requests/` | 11월 프론트엔드 API 요청서 | -| `item-master-archived/` | 분석, 구현, 디자인 문서 | -| `item-master-spec.md` | 초기 API 명세 (11-20) | -| `item-master-gap-analysis.md` | 갭 분석 문서 | - ---- - -## 📊 관련 테스트 - -| 파일 | 위치 | 용도 | -|------|------|------| -| `item-master-init-api-flow.json` | `docs/plans/flow-tests/` | 초기화 API 테스트 | -| `item-master-page-api-flow.json` | `docs/plans/flow-tests/` | 페이지 CRUD 테스트 | -| `item-master-field-api-flow.json` | `docs/plans/flow-tests/` | 필드 CRUD 테스트 | -| `item-fields-is-active-test.json` | `api/docs/api-flows/` | 활성 필터 테스트 | - ---- - -## 🔗 참고 문서 - -- **명명 규칙**: `docs/plans/items-naming-convention.md` -- **마이그레이션**: `docs/guides/item-management-migration.md` -- **DB 분석**: `docs/data/analysis/item-db-analysis.md` -- **MES 분석**: `docs/projects/mes/00_baseline/docs_breakdown/api_item_analysis_summary.md` - ---- - -## 변경 이력 - -| 날짜 | 내용 | -|------|------| -| 2025-12-09 | ItemTypeSeeder 구현 완료 (attributes.source_table 추가), DB 구조 확정 기록 추가, field-integration.md v1.3 반영 | -| 2025-12-09 | 핵심 개념 정의 (item_type/source_table/ref_type) 추가, 문서 관계도 추가 | -| 2025-12-09 | items-api-unified-plan.md 추가, 핵심 문서 테이블 갱신 | -| 2025-12-09 | 문서 인덱스 생성, 중복 문서 정리 | diff --git a/specs/item-master-field-integration.md b/specs/item-master-field-integration.md deleted file mode 100644 index a784046..0000000 --- a/specs/item-master-field-integration.md +++ /dev/null @@ -1,1238 +0,0 @@ -# ItemMaster 범용 메타 필드 시스템 구현 계획 - -**작성일**: 2025-12-08 -**버전**: v1.3 -**상태**: 구현 중 (DB 구조 확정) - ---- - -## 1. 개요 - -### 1.1 목적 -ItemMaster를 **범용 메타 필드 정의 시스템**으로 확장하여, 다양한 도메인(제품, 자재, 회계, 생산 등)의 필드를 동일한 구조로 관리 - -### 1.2 핵심 원칙 -| 항목 | 방침 | -|------|------| -| **프론트엔드** | 변경 없음 | -| **API 응답** | 변경 없음 (매핑 정보 미노출) | -| **DB 스키마** | `common_codes`로 도메인 관리, `source_table`로 테이블 분기 | -| **백엔드 서비스** | `page.source_table`로 테이블 분기, 저장 시 자동 분배 | - -### 1.3 적용 대상 테이블 (1차) -- `products` - 제품 (FG, PT) -- `materials` - 자재 (SM, RM, CS) -- `product_components` - BOM -- `material_inspections` - 자재 검수 -- `material_inspection_items` - 검수 항목 -- `material_receipts` - 자재 입고 - -### 1.4 향후 확장 예정 -- `journals` - 회계 전표 -- `work_orders` - 생산 지시 -- `quality_controls` - 품질 관리 -- 기타 도메인 테이블 - ---- - -## 2. 분기 로직 플로우 - -### 2.1 현재 구조 (✅ 구현 완료) - -> **참고**: 아래 구조는 이미 DB에 구현되어 운영 중입니다. - -#### 2.1.1 common_codes (item_type 마스터) - -``` -common_codes (code_group = 'item_type') - ✅ 시딩 완료 -┌────────────┬────────┬──────────┬─────────────────────────────────────────┐ -│ code_group │ code │ name │ attributes (JSON) │ -├────────────┼────────┼──────────┼─────────────────────────────────────────┤ -│ item_type │ FG │ 완제품 │ {"source_table":"products","name_en":...}│ -│ item_type │ PT │ 부품 │ {"source_table":"products","name_en":...}│ -│ item_type │ SM │ 부자재 │ {"source_table":"materials","name_en":..}│ -│ item_type │ RM │ 원자재 │ {"source_table":"materials","name_en":..}│ -│ item_type │ CS │ 소모품 │ {"source_table":"materials","name_en":..}│ -└────────────┴────────┴──────────┴─────────────────────────────────────────┘ - -→ attributes.source_table: 물리 테이블 매핑 정보 -→ FG/PT → products, SM/RM/CS → materials -``` - -#### 2.1.2 item_pages (페이지 설정) - -``` -item_pages - ✅ 테이블 존재 (source_table 컬럼 포함) -┌────┬───────────┬──────────┬────────────┬──────────────┬───────────────────┐ -│ id │ tenant_id │ group_id │ page_name │ item_type │ source_table │ -├────┼───────────┼──────────┼────────────┼──────────────┼───────────────────┤ -│ 1 │ 287 │ 1 │ 완제품기본 │ FG │ products │ -│ 2 │ 287 │ 1 │ 완제품상세 │ FG │ products │ ← 같은 FG로 여러 페이지! -│ 3 │ 287 │ 1 │ 부품관리 │ PT │ products │ -│ 4 │ 287 │ 1 │ 부자재 │ SM │ materials │ -└────┴───────────┴──────────┴────────────┴──────────────┴───────────────────┘ - -→ page_name: 유지 (테넌트별 페이지명 커스터마이징) -→ source_table: 성능을 위해 중복 저장 (common_codes에서도 조회 가능) -→ 같은 item_type으로 여러 페이지 생성 가능 -``` - -#### 2.1.3 entity_relationships (N:M 링크) - -``` -entity_relationships - ✅ 테이블 존재 -┌────┬─────────────┬───────────┬─────────────┬──────────┐ -│ id │ parent_type │ parent_id │ child_type │ child_id │ -├────┼─────────────┼───────────┼─────────────┼──────────┤ -│ 1 │ page │ 981 │ section │ 1 │ -│ 2 │ page │ 981 │ section │ 2 │ -│ 3 │ section │ 1 │ field │ 1 │ -└────┴─────────────┴───────────┴─────────────┴──────────┘ - -→ 독립 엔티티 아키텍처 -→ page → section → field 관계를 링크 테이블로 관리 -``` - -### 2.2 아키텍처 관계도 - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ ItemMaster 테이블 구조 │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────────────────────────────────┐ │ -│ │ common_codes (마스터 코드) │ │ -│ │ code_group='item_type' │ │ -│ │ ────────────────────── │ │ -│ │ FG → attributes.source_table │ │ -│ │ PT → attributes.source_table │ │ -│ │ SM/RM/CS → attributes.source_table │ │ -│ └──────────────┬───────────────────────┘ │ -│ │ 참조 (item_type) │ -│ ▼ │ -│ ┌──────────────────────────────────────┐ │ -│ │ item_pages (페이지 설정) │ │ -│ │ ────────────────────── │ │ -│ │ - 테넌트별 페이지 구성 │ │ -│ │ - 같은 item_type으로 N개 페이지 │ │ -│ │ - source_table (성능용 중복 저장) │ │ -│ └──────────────┬───────────────────────┘ │ -│ │ parent_id │ -│ ▼ │ -│ ┌──────────────────────────────────────┐ │ -│ │ entity_relationships (N:M 링크) │ │ -│ │ ────────────────────── │ │ -│ │ page → section → field │ │ -│ └──────────────┬───────────────────────┘ │ -│ │ │ -│ ┌───────┴───────┐ │ -│ ▼ ▼ │ -│ ┌────────────┐ ┌────────────┐ │ -│ │item_sections│ │item_fields │ │ -│ │(독립 엔티티)│ │(독립 엔티티)│ │ -│ └────────────┘ └────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -### 2.3 source_table 조회 방법 - -```php -// 방법 1: item_pages에서 직접 조회 (성능 우선) -$page = ItemPage::find($pageId); -$sourceTable = $page->source_table; // 'products' or 'materials' - -// 방법 2: common_codes에서 조회 (정규화 우선) -$code = CommonCode::where('code_group', 'item_type') - ->where('code', $itemType) - ->first(); -$sourceTable = $code->attributes['source_table']; -``` - -#### 2.2.4 향후 테이블 분리 확장 예시 - -``` -나중에 item_type별로 다른 테이블 사용이 필요할 경우: - -현재: - FG → source_table = 'products' - PT → source_table = 'products' - -확장 가능: - FG → source_table = 'finished_goods' (별도 테이블) - PT → source_table = 'semi_products' (별도 테이블) - -→ source_table만 변경하면 테이블 스위칭 가능 -→ item_type은 그대로 유지 (프론트엔드 변경 없음) -``` - -### 2.3 데이터 저장 플로우 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ [프론트엔드] │ -│ │ │ -│ ▼ │ -│ 1. 페이지 선택 (page_id = 1, 완제품) │ -│ │ │ -│ ▼ │ -│ 2. 필드 입력 후 저장 │ -│ │ │ -│ ▼ │ -│ POST /item-master/data │ -│ { │ -│ "page_id": 1, │ -│ "field_values": { │ -│ "1": "FG-001", ← 품목코드 │ -│ "2": "완제품A", ← 품목명 │ -│ "3": "EA" ← 단위 │ -│ } │ -│ } │ -│ │ │ -│ ▼ │ -│ [백엔드] │ -│ │ │ -│ ▼ │ -│ 3. page_id → source_table 조회 ('products') │ -│ │ │ -│ ▼ │ -│ 4. source_table = 'products' → products 테이블에 저장 │ -│ │ │ -│ ▼ │ -│ 5. 필드별 source_column 매핑 │ -│ field_id=1 → source_column='code' │ -│ field_id=2 → source_column='name' │ -│ field_id=3 → source_column='unit' │ -│ │ │ -│ ▼ │ -│ 6. INSERT INTO products (code, name, unit) VALUES (...) │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 2.4 향후 확장 예시 (회계) - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ [프론트엔드] - 동일한 ItemMaster UI 사용 │ -│ │ │ -│ ▼ │ -│ POST /item-master/data │ -│ { │ -│ "page_id": 6, ← 회계전표 페이지 │ -│ "field_values": { │ -│ "101": "2025-12-08", ← 전표일자 │ -│ "102": "매출", ← 전표유형 │ -│ "103": 1000000 ← 금액 │ -│ } │ -│ } │ -│ │ │ -│ ▼ │ -│ [백엔드] │ -│ │ │ -│ ▼ │ -│ page_id=6 → source_table='journals' → journals 테이블에 저장 │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 3. 현재 테이블 스키마 분석 - -### 3.1 products (31 컬럼) - -| 컬럼명 | 타입 | 설명 | ItemMaster 필드 타입 | -|--------|------|------|---------------------| -| code | varchar(50) | 품목코드 | textbox (필수) | -| name | varchar(255) | 품목명 | textbox (필수) | -| unit | varchar(20) | 단위 | dropdown (필수) | -| product_type | varchar(20) | 제품유형 (FG/PT) | dropdown | -| category_id | bigint | 카테고리 | dropdown | -| is_sellable | tinyint(1) | 판매가능 | checkbox | -| is_purchasable | tinyint(1) | 구매가능 | checkbox | -| is_producible | tinyint(1) | 생산가능 | checkbox | -| is_active | tinyint(1) | 활성화 | checkbox | -| certification_number | varchar(100) | 인증번호 | textbox | -| certification_date | date | 인증일자 | date | -| certification_expiry | date | 인증만료일 | date | -| bending_diagram_file_id | bigint | 밴딩도면 파일 | file | -| specification_file_id | bigint | 시방서 파일 | file | -| certification_file_id | bigint | 인증서 파일 | file | -| attributes | json | 동적 속성 | (커스텀 필드 저장용) | - -### 3.2 materials (20 컬럼) - -| 컬럼명 | 타입 | 설명 | ItemMaster 필드 타입 | -|--------|------|------|---------------------| -| material_code | varchar(50) | 자재코드 | textbox (필수) | -| name | varchar(255) | 자재명 | textbox (필수) | -| item_name | varchar(255) | 품목명 | textbox | -| specification | varchar(255) | 규격 | textbox | -| unit | varchar(20) | 단위 | dropdown (필수) | -| category_id | bigint | 카테고리 | dropdown | -| is_inspection | tinyint(1) | 검수필요 | checkbox | -| search_tag | text | 검색태그 | textarea | -| attributes | json | 동적 속성 | (커스텀 필드 저장용) | -| options | json | 옵션 | (커스텀 필드 저장용) | - -### 3.3 product_components (15 컬럼) - BOM - -| 컬럼명 | 타입 | 설명 | ItemMaster 필드 타입 | -|--------|------|------|---------------------| -| parent_product_id | bigint | 상위제품 | lookup | -| ref_type | varchar(20) | 참조유형 (product/material) | dropdown | -| ref_id | bigint | 참조ID | lookup | -| quantity | decimal(18,6) | 수량 | number (필수) | -| formula | varchar(500) | 계산공식 | textbox | -| sort_order | int | 정렬순서 | number | -| note | text | 비고 | textarea | - -### 3.4 material_inspections (14 컬럼) - -| 컬럼명 | 타입 | 설명 | ItemMaster 필드 타입 | -|--------|------|------|---------------------| -| material_id | bigint | 자재ID | lookup | -| inspection_date | date | 검수일 | date (필수) | -| inspector_id | bigint | 검수자 | dropdown | -| status | varchar(20) | 상태 | dropdown | -| lot_no | varchar(50) | LOT번호 | textbox | -| quantity | decimal(15,4) | 검수수량 | number | -| passed_quantity | decimal(15,4) | 합격수량 | number | -| rejected_quantity | decimal(15,4) | 불합격수량 | number | -| note | text | 비고 | textarea | - -### 3.5 material_inspection_items (9 컬럼) - -| 컬럼명 | 타입 | 설명 | ItemMaster 필드 타입 | -|--------|------|------|---------------------| -| inspection_id | bigint | 검수ID | lookup | -| check_item | varchar(255) | 점검항목 | textbox (필수) | -| standard | varchar(255) | 기준 | textbox | -| result | varchar(20) | 결과 | dropdown | -| measured_value | varchar(100) | 측정값 | textbox | -| note | text | 비고 | textarea | - -### 3.6 material_receipts (18 컬럼) - -| 컬럼명 | 타입 | 설명 | ItemMaster 필드 타입 | -|--------|------|------|---------------------| -| material_id | bigint | 자재ID | lookup | -| receipt_date | date | 입고일 | date (필수) | -| lot_no | varchar(50) | LOT번호 | textbox | -| quantity | decimal(15,4) | 입고수량 | number (필수) | -| unit_price | decimal(15,4) | 단가 | number | -| total_price | decimal(15,4) | 금액 | number | -| supplier_id | bigint | 공급업체 | dropdown | -| warehouse_id | bigint | 입고창고 | dropdown | -| po_number | varchar(50) | 발주번호 | textbox | -| invoice_number | varchar(50) | 송장번호 | textbox | -| note | text | 비고 | textarea | - ---- - -## 4. DB 스키마 변경 - -### 4.1 마이그레이션: item_fields 확장 - -```php -string('source_table', 100) - ->nullable() - ->after('properties') - ->comment('내부용: 원본 테이블명 (products, materials 등)'); - - $table->string('source_column', 100) - ->nullable() - ->after('source_table') - ->comment('내부용: 원본 컬럼명 (code, name 등)'); - - $table->enum('storage_type', ['column', 'json']) - ->default('json') - ->after('source_column') - ->comment('내부용: 저장방식 (column=DB컬럼, json=attributes/options)'); - - $table->string('json_path', 200) - ->nullable() - ->after('storage_type') - ->comment('내부용: JSON 저장 경로 (예: attributes.custom_size)'); - - // 인덱스 - $table->index(['source_table', 'source_column'], 'idx_source_mapping'); - }); - } - - public function down(): void - { - Schema::table('item_fields', function (Blueprint $table) { - $table->dropIndex('idx_source_mapping'); - $table->dropColumn(['source_table', 'source_column', 'storage_type', 'json_path']); - }); - } -}; -``` - -### 4.2 컬럼 설명 - -| 컬럼 | 타입 | 용도 | -|------|------|------| -| `source_table` | varchar(100) | 원본 테이블명 (NULL이면 커스텀 필드) | -| `source_column` | varchar(100) | 원본 컬럼명 | -| `storage_type` | enum | `column`: DB 컬럼 직접 저장, `json`: JSON 필드에 저장 | -| `json_path` | varchar(200) | JSON 저장 시 경로 (예: `attributes.custom_size`) | - -### 4.3 item_pages 테이블 (✅ 이미 구현됨) - -> **참고**: item_pages 테이블은 이미 source_table 컬럼이 추가되어 있습니다. -> page_name은 유지됩니다 (테넌트별 페이지명 커스터마이징 필요). - -```sql --- 현재 item_pages 구조 -CREATE TABLE item_pages ( - id BIGINT PRIMARY KEY, - tenant_id BIGINT NOT NULL, - group_id INT DEFAULT 1, - page_name VARCHAR(100), -- 유지 (테넌트별 커스텀 가능) - item_type ENUM('FG','PT','SM','RM','CS'), - source_table VARCHAR(100), -- ✅ 이미 추가됨 - absolute_path VARCHAR(500), - is_active TINYINT(1) DEFAULT 1, - ... -); -``` - -### 4.4 common_codes 시더 (item_type) - ✅ 구현 완료 - -```php - 'item_type', - 'code' => 'FG', - 'name' => '완제품', - 'tenant_id' => $tenantId, - 'attributes' => json_encode([ - 'source_table' => 'products', - 'name_en' => 'Finished Goods', - ]), - ], - [ - 'code_group' => 'item_type', - 'code' => 'PT', - 'name' => '부품', - 'tenant_id' => $tenantId, - 'attributes' => json_encode([ - 'source_table' => 'products', - 'name_en' => 'Parts', - ]), - ], - [ - 'code_group' => 'item_type', - 'code' => 'SM', - 'name' => '부자재', - 'tenant_id' => $tenantId, - 'attributes' => json_encode([ - 'source_table' => 'materials', - 'name_en' => 'Sub-Materials', - ]), - ], - [ - 'code_group' => 'item_type', - 'code' => 'RM', - 'name' => '원자재', - 'tenant_id' => $tenantId, - 'attributes' => json_encode([ - 'source_table' => 'materials', - 'name_en' => 'Raw Materials', - ]), - ], - [ - 'code_group' => 'item_type', - 'code' => 'CS', - 'name' => '소모품', - 'tenant_id' => $tenantId, - 'attributes' => json_encode([ - 'source_table' => 'materials', - 'name_en' => 'Consumables', - ]), - ], - ]; - - foreach ($itemTypes as $index => $item) { - DB::table('common_codes')->updateOrInsert( - [ - 'code_group' => $item['code_group'], - 'code' => $item['code'], - 'tenant_id' => $item['tenant_id'], - ], - array_merge($item, [ - 'sort_order' => $index + 1, - 'is_active' => true, - 'created_at' => now(), - 'updated_at' => now(), - ]) - ); - } - } -} - -// 실행: php artisan db:seed --class=ItemTypeSeeder -``` - ---- - -## 5. 모델 수정 - -### 5.1 ItemField 모델 - -```php - 'boolean', - 'display_condition' => 'array', - 'validation_rules' => 'array', - 'options' => 'array', - 'properties' => 'array', - ]; - - /** - * API 응답에서 제외할 컬럼 (내부용) - */ - protected $hidden = [ - 'source_table', - 'source_column', - 'storage_type', - 'json_path', - ]; - - /** - * 시스템 필드 여부 확인 - */ - public function isSystemField(): bool - { - return !is_null($this->source_table) && !is_null($this->source_column); - } - - /** - * 컬럼 직접 저장 여부 - */ - public function isColumnStorage(): bool - { - return $this->storage_type === 'column'; - } - - /** - * JSON 저장 여부 - */ - public function isJsonStorage(): bool - { - return $this->storage_type === 'json'; - } -} -``` - ---- - -## 6. 시딩 데이터 - -### 6.1 시더 클래스 - -```php -getProductFields($tenantId), - $this->getMaterialFields($tenantId), - $this->getBomFields($tenantId), - $this->getInspectionFields($tenantId), - $this->getReceiptFields($tenantId) - ); - - foreach ($systemFields as $field) { - DB::table('item_fields')->updateOrInsert( - [ - 'tenant_id' => $field['tenant_id'], - 'source_table' => $field['source_table'], - 'source_column' => $field['source_column'], - ], - $field - ); - } - } - - private function getProductFields(int $tenantId): array - { - $baseFields = [ - 'tenant_id' => $tenantId, - 'source_table' => 'products', - 'storage_type' => 'column', - 'created_at' => now(), - 'updated_at' => now(), - ]; - - return [ - array_merge($baseFields, [ - 'source_column' => 'code', - 'field_name' => '품목코드', - 'field_type' => 'textbox', - 'is_required' => true, - 'order_no' => 1, - ]), - array_merge($baseFields, [ - 'source_column' => 'name', - 'field_name' => '품목명', - 'field_type' => 'textbox', - 'is_required' => true, - 'order_no' => 2, - ]), - array_merge($baseFields, [ - 'source_column' => 'unit', - 'field_name' => '단위', - 'field_type' => 'dropdown', - 'is_required' => true, - 'order_no' => 3, - ]), - array_merge($baseFields, [ - 'source_column' => 'product_type', - 'field_name' => '제품유형', - 'field_type' => 'dropdown', - 'order_no' => 4, - 'options' => json_encode([ - ['label' => '완제품', 'value' => 'FG'], - ['label' => '반제품', 'value' => 'PT'], - ]), - ]), - array_merge($baseFields, [ - 'source_column' => 'category_id', - 'field_name' => '카테고리', - 'field_type' => 'dropdown', - 'order_no' => 5, - ]), - array_merge($baseFields, [ - 'source_column' => 'is_sellable', - 'field_name' => '판매가능', - 'field_type' => 'checkbox', - 'order_no' => 6, - 'default_value' => 'true', - ]), - array_merge($baseFields, [ - 'source_column' => 'is_purchasable', - 'field_name' => '구매가능', - 'field_type' => 'checkbox', - 'order_no' => 7, - 'default_value' => 'false', - ]), - array_merge($baseFields, [ - 'source_column' => 'is_producible', - 'field_name' => '생산가능', - 'field_type' => 'checkbox', - 'order_no' => 8, - 'default_value' => 'true', - ]), - array_merge($baseFields, [ - 'source_column' => 'is_active', - 'field_name' => '활성화', - 'field_type' => 'checkbox', - 'order_no' => 9, - 'default_value' => 'true', - ]), - array_merge($baseFields, [ - 'source_column' => 'certification_number', - 'field_name' => '인증번호', - 'field_type' => 'textbox', - 'order_no' => 10, - ]), - array_merge($baseFields, [ - 'source_column' => 'certification_date', - 'field_name' => '인증일자', - 'field_type' => 'date', - 'order_no' => 11, - ]), - array_merge($baseFields, [ - 'source_column' => 'certification_expiry', - 'field_name' => '인증만료일', - 'field_type' => 'date', - 'order_no' => 12, - ]), - ]; - } - - private function getMaterialFields(int $tenantId): array - { - $baseFields = [ - 'tenant_id' => $tenantId, - 'source_table' => 'materials', - 'storage_type' => 'column', - 'created_at' => now(), - 'updated_at' => now(), - ]; - - return [ - array_merge($baseFields, [ - 'source_column' => 'material_code', - 'field_name' => '자재코드', - 'field_type' => 'textbox', - 'is_required' => true, - 'order_no' => 1, - ]), - array_merge($baseFields, [ - 'source_column' => 'name', - 'field_name' => '자재명', - 'field_type' => 'textbox', - 'is_required' => true, - 'order_no' => 2, - ]), - array_merge($baseFields, [ - 'source_column' => 'item_name', - 'field_name' => '품목명', - 'field_type' => 'textbox', - 'order_no' => 3, - ]), - array_merge($baseFields, [ - 'source_column' => 'specification', - 'field_name' => '규격', - 'field_type' => 'textbox', - 'order_no' => 4, - ]), - array_merge($baseFields, [ - 'source_column' => 'unit', - 'field_name' => '단위', - 'field_type' => 'dropdown', - 'is_required' => true, - 'order_no' => 5, - ]), - array_merge($baseFields, [ - 'source_column' => 'category_id', - 'field_name' => '카테고리', - 'field_type' => 'dropdown', - 'order_no' => 6, - ]), - array_merge($baseFields, [ - 'source_column' => 'is_inspection', - 'field_name' => '검수필요', - 'field_type' => 'checkbox', - 'order_no' => 7, - 'default_value' => 'false', - ]), - array_merge($baseFields, [ - 'source_column' => 'search_tag', - 'field_name' => '검색태그', - 'field_type' => 'textarea', - 'order_no' => 8, - ]), - ]; - } - - private function getBomFields(int $tenantId): array - { - $baseFields = [ - 'tenant_id' => $tenantId, - 'source_table' => 'product_components', - 'storage_type' => 'column', - 'created_at' => now(), - 'updated_at' => now(), - ]; - - return [ - array_merge($baseFields, [ - 'source_column' => 'ref_type', - 'field_name' => '참조유형', - 'field_type' => 'dropdown', - 'order_no' => 1, - 'options' => json_encode([ - ['label' => '제품', 'value' => 'product'], - ['label' => '자재', 'value' => 'material'], - ]), - ]), - array_merge($baseFields, [ - 'source_column' => 'ref_id', - 'field_name' => '참조품목', - 'field_type' => 'dropdown', - 'order_no' => 2, - ]), - array_merge($baseFields, [ - 'source_column' => 'quantity', - 'field_name' => '수량', - 'field_type' => 'number', - 'is_required' => true, - 'order_no' => 3, - 'properties' => json_encode(['precision' => 6]), - ]), - array_merge($baseFields, [ - 'source_column' => 'formula', - 'field_name' => '계산공식', - 'field_type' => 'textbox', - 'order_no' => 4, - ]), - array_merge($baseFields, [ - 'source_column' => 'note', - 'field_name' => '비고', - 'field_type' => 'textarea', - 'order_no' => 5, - ]), - ]; - } - - private function getInspectionFields(int $tenantId): array - { - $baseFields = [ - 'tenant_id' => $tenantId, - 'source_table' => 'material_inspections', - 'storage_type' => 'column', - 'created_at' => now(), - 'updated_at' => now(), - ]; - - return [ - array_merge($baseFields, [ - 'source_column' => 'inspection_date', - 'field_name' => '검수일', - 'field_type' => 'date', - 'is_required' => true, - 'order_no' => 1, - ]), - array_merge($baseFields, [ - 'source_column' => 'inspector_id', - 'field_name' => '검수자', - 'field_type' => 'dropdown', - 'order_no' => 2, - ]), - array_merge($baseFields, [ - 'source_column' => 'status', - 'field_name' => '검수상태', - 'field_type' => 'dropdown', - 'order_no' => 3, - 'options' => json_encode([ - ['label' => '대기', 'value' => 'pending'], - ['label' => '진행중', 'value' => 'in_progress'], - ['label' => '완료', 'value' => 'completed'], - ['label' => '불합격', 'value' => 'rejected'], - ]), - ]), - array_merge($baseFields, [ - 'source_column' => 'lot_no', - 'field_name' => 'LOT번호', - 'field_type' => 'textbox', - 'order_no' => 4, - ]), - array_merge($baseFields, [ - 'source_column' => 'quantity', - 'field_name' => '검수수량', - 'field_type' => 'number', - 'order_no' => 5, - ]), - array_merge($baseFields, [ - 'source_column' => 'passed_quantity', - 'field_name' => '합격수량', - 'field_type' => 'number', - 'order_no' => 6, - ]), - array_merge($baseFields, [ - 'source_column' => 'rejected_quantity', - 'field_name' => '불합격수량', - 'field_type' => 'number', - 'order_no' => 7, - ]), - array_merge($baseFields, [ - 'source_column' => 'note', - 'field_name' => '비고', - 'field_type' => 'textarea', - 'order_no' => 8, - ]), - ]; - } - - private function getReceiptFields(int $tenantId): array - { - $baseFields = [ - 'tenant_id' => $tenantId, - 'source_table' => 'material_receipts', - 'storage_type' => 'column', - 'created_at' => now(), - 'updated_at' => now(), - ]; - - return [ - array_merge($baseFields, [ - 'source_column' => 'receipt_date', - 'field_name' => '입고일', - 'field_type' => 'date', - 'is_required' => true, - 'order_no' => 1, - ]), - array_merge($baseFields, [ - 'source_column' => 'lot_no', - 'field_name' => 'LOT번호', - 'field_type' => 'textbox', - 'order_no' => 2, - ]), - array_merge($baseFields, [ - 'source_column' => 'quantity', - 'field_name' => '입고수량', - 'field_type' => 'number', - 'is_required' => true, - 'order_no' => 3, - ]), - array_merge($baseFields, [ - 'source_column' => 'unit_price', - 'field_name' => '단가', - 'field_type' => 'number', - 'order_no' => 4, - 'properties' => json_encode(['precision' => 4]), - ]), - array_merge($baseFields, [ - 'source_column' => 'total_price', - 'field_name' => '금액', - 'field_type' => 'number', - 'order_no' => 5, - 'properties' => json_encode(['precision' => 4]), - ]), - array_merge($baseFields, [ - 'source_column' => 'supplier_id', - 'field_name' => '공급업체', - 'field_type' => 'dropdown', - 'order_no' => 6, - ]), - array_merge($baseFields, [ - 'source_column' => 'warehouse_id', - 'field_name' => '입고창고', - 'field_type' => 'dropdown', - 'order_no' => 7, - ]), - array_merge($baseFields, [ - 'source_column' => 'po_number', - 'field_name' => '발주번호', - 'field_type' => 'textbox', - 'order_no' => 8, - ]), - array_merge($baseFields, [ - 'source_column' => 'invoice_number', - 'field_name' => '송장번호', - 'field_type' => 'textbox', - 'order_no' => 9, - ]), - array_merge($baseFields, [ - 'source_column' => 'note', - 'field_name' => '비고', - 'field_type' => 'textarea', - 'order_no' => 10, - ]), - ]; - } -} -``` - ---- - -## 7. 서비스 로직 (데이터 저장) - -### 7.1 ItemDataService (신규) - -```php - value] 형태 - * @param int|null $recordId 수정 시 레코드 ID - * @return array 저장된 데이터 - */ - public function saveData(string $sourceTable, array $fieldValues, ?int $recordId = null): array - { - // 해당 테이블의 필드 매핑 정보 조회 - $fields = ItemField::where('tenant_id', $this->tenantId()) - ->where('source_table', $sourceTable) - ->get() - ->keyBy('id'); - - $columnData = []; // DB 컬럼 직접 저장 - $jsonData = []; // JSON (attributes/options) 저장 - - foreach ($fieldValues as $fieldId => $value) { - $field = $fields->get($fieldId); - - if (!$field) { - // 시스템 필드가 아닌 커스텀 필드 - $customField = ItemField::find($fieldId); - if ($customField) { - $jsonPath = $customField->json_path ?? "attributes.{$customField->field_name}"; - data_set($jsonData, $jsonPath, $value); - } - continue; - } - - if ($field->isColumnStorage()) { - // DB 컬럼에 직접 저장 - $columnData[$field->source_column] = $this->castValue($value, $field); - } else { - // JSON 필드에 저장 - $jsonPath = $field->json_path ?? "attributes.{$field->field_name}"; - data_set($jsonData, $jsonPath, $value); - } - } - - // JSON 데이터 병합 - if (!empty($jsonData['attributes'])) { - $columnData['attributes'] = json_encode($jsonData['attributes']); - } - if (!empty($jsonData['options'])) { - $columnData['options'] = json_encode($jsonData['options']); - } - - // 공통 컬럼 추가 - $columnData['tenant_id'] = $this->tenantId(); - $columnData['updated_by'] = $this->apiUserId(); - - if ($recordId) { - // 수정 - DB::table($sourceTable) - ->where('tenant_id', $this->tenantId()) - ->where('id', $recordId) - ->update($columnData); - - return array_merge(['id' => $recordId], $columnData); - } else { - // 생성 - $columnData['created_by'] = $this->apiUserId(); - $id = DB::table($sourceTable)->insertGetId($columnData); - - return array_merge(['id' => $id], $columnData); - } - } - - /** - * 필드 타입에 따른 값 변환 - */ - private function castValue($value, ItemField $field) - { - return match ($field->field_type) { - 'number' => is_numeric($value) ? (float) $value : null, - 'checkbox' => filter_var($value, FILTER_VALIDATE_BOOLEAN), - 'date' => $value ? date('Y-m-d', strtotime($value)) : null, - default => $value, - }; - } - - /** - * 레코드 조회 시 필드 매핑 적용 - */ - public function getData(string $sourceTable, int $recordId): array - { - $record = DB::table($sourceTable) - ->where('tenant_id', $this->tenantId()) - ->where('id', $recordId) - ->first(); - - if (!$record) { - return []; - } - - // 필드 매핑 정보 조회 - $fields = ItemField::where('tenant_id', $this->tenantId()) - ->where('source_table', $sourceTable) - ->get(); - - $result = []; - $attributes = json_decode($record->attributes ?? '{}', true); - $options = json_decode($record->options ?? '{}', true); - - foreach ($fields as $field) { - if ($field->isColumnStorage()) { - $result[$field->id] = $record->{$field->source_column} ?? null; - } else { - $jsonPath = $field->json_path ?? "attributes.{$field->field_name}"; - $result[$field->id] = data_get( - ['attributes' => $attributes, 'options' => $options], - $jsonPath - ); - } - } - - return $result; - } -} -``` - ---- - -## 8. API 영향 없음 확인 - -### 8.1 기존 API 응답 (변경 없음) - -```json -// GET /api/v1/item-master/init -{ - "success": true, - "message": "message.fetched", - "data": { - "pages": [{ - "id": 1, - "page_name": "기본정보", - "item_type": "FG", - "sections": [{ - "id": 1, - "title": "품목코드 정보", - "fields": [ - { - "id": 1, - "field_name": "품목코드", - "field_type": "textbox", - "is_required": true, - "order_no": 1 - // source_table, source_column 등은 $hidden으로 제외됨 - } - ] - }] - }] - } -} -``` - -### 8.2 프론트엔드 (변경 없음) - -- 기존 ItemMaster API 그대로 사용 -- 필드 정의 조회/수정 동일 -- 품목 데이터 저장 시 기존 Products/Materials API 사용 - ---- - -## 9. 구현 순서 - -| 순서 | 작업 | 예상 시간 | 담당 | -|------|------|----------|------| -| 1 | 마이그레이션 파일 생성 및 실행 | 30분 | Backend | -| 2 | ItemField 모델 수정 ($hidden 추가) | 15분 | Backend | -| 3 | 시더 클래스 생성 | 1시간 | Backend | -| 4 | 시딩 실행 및 데이터 확인 | 30분 | Backend | -| 5 | ItemDataService 구현 | 2시간 | Backend | -| 6 | 기존 ProductService/MaterialService 연동 | 2시간 | Backend | -| 7 | 테스트 | 1시간 | Backend | - -**총 예상 시간: 7~8시간 (1일)** - ---- - -## 10. 향후 확장 - -### 10.1 신규 도메인 추가 시 -1. 대상 테이블 스키마 분석 -2. 시더에 필드 매핑 추가 -3. 시딩 실행 -4. (필요시) ItemDataService에 특수 로직 추가 - -### 10.2 예정 도메인 -- [ ] 회계 (accounts, journals, ledgers) -- [ ] 생산 (work_orders, production_records) -- [ ] 재고 (inventories, stock_movements) -- [ ] 품질 (quality_controls, defect_reports) - ---- - -## 11. 체크리스트 - -### 구현 전 -- [ ] 현재 item_fields 테이블 구조 확인 -- [ ] 마이그레이션 롤백 계획 수립 -- [ ] 기존 데이터 백업 - -### 구현 중 -- [ ] 마이그레이션 실행 -- [ ] 모델 $hidden 적용 -- [ ] 시더 실행 -- [ ] API 응답 검증 (매핑 컬럼 미노출 확인) - -### 구현 후 -- [ ] 기존 ItemMaster API 정상 동작 확인 -- [ ] 프론트엔드 영향 없음 확인 -- [ ] 품목 저장 시 매핑 정상 동작 확인 - ---- - -**문서 끝** diff --git a/specs/item-master-field-key-validation.md b/specs/item-master-field-key-validation.md deleted file mode 100644 index c035c59..0000000 --- a/specs/item-master-field-key-validation.md +++ /dev/null @@ -1,200 +0,0 @@ -# Item Master field_key 검증 정책 - -## 개요 - -field_key 저장 및 검증 정책을 변경하여 시스템 필드(고정 컬럼)와의 충돌을 방지합니다. - -## 변경 사항 - -### 1. field_key 저장 정책 변경 - -**변경 전:** -``` -field_key = {id}_{입력값} -예: 98_code, 99_name -``` - -**변경 후:** -``` -field_key = {입력값} -예: code, name (단, 시스템 예약어는 사용 불가) -``` - -### 2. 시스템 필드 예약어 검증 추가 - -#### 검증 흐름 -``` -field_key 입력 - ↓ -source_table 확인 (products / materials) - ↓ -해당 테이블 예약어 체크 - ↓ -기존 필드 중복 체크 - ↓ -저장 -``` - -#### source_table 기반 예약어 매핑 - -| source_table | 대상 테이블 | 예약어 목록 | -|--------------|-------------|-------------| -| `products` | products | code, name, unit, product_type, ... | -| `materials` | materials | name, material_code, material_type, ... | -| `null` | 전체 | products + materials 예약어 모두 체크 (안전 모드) | - -## 구현 상세 - -### 파일 구조 - -``` -app/ -├── Constants/ -│ └── SystemFields.php # 신규: 예약어 상수 클래스 -└── Services/ - └── ItemMaster/ - └── ItemFieldService.php # 수정: 예약어 검증 추가 -``` - -### SystemFields 상수 클래스 - -```php -// app/Constants/SystemFields.php - -class SystemFields -{ - // 소스 테이블 상수 - public const SOURCE_TABLE_PRODUCTS = 'products'; - public const SOURCE_TABLE_MATERIALS = 'materials'; - - // 그룹 ID 상수 - public const GROUP_ITEM_MASTER = 1; - - // products 테이블 고정 컬럼 - public const PRODUCTS = [ - 'code', 'name', 'unit', 'category_id', 'product_type', 'description', - 'is_sellable', 'is_purchasable', 'is_producible', 'is_variable_size', 'is_active', - 'safety_stock', 'lead_time', 'product_category', 'part_type', - 'bending_diagram', 'bending_details', - 'specification_file', 'specification_file_name', - 'certification_file', 'certification_file_name', - 'certification_number', 'certification_start_date', 'certification_end_date', - 'attributes', 'attributes_archive', - ]; - - // materials 테이블 고정 컬럼 - public const MATERIALS = [ - 'name', 'item_name', 'specification', 'material_code', 'material_type', - 'unit', 'category_id', 'is_inspection', 'is_active', - 'search_tag', 'remarks', 'attributes', 'options', - ]; - - // 공통 시스템 컬럼 - public const COMMON = [ - 'id', 'tenant_id', 'created_by', 'updated_by', 'deleted_by', - 'created_at', 'updated_at', 'deleted_at', - ]; - - // source_table 기반 예약어 조회 - public static function getReservedKeys(string $sourceTable): array; - - // 예약어 여부 확인 - public static function isReserved(string $fieldKey, string $sourceTable): bool; - - // 그룹 내 전체 예약어 조회 (안전 모드) - public static function getAllReservedKeysForGroup(int $groupId): array; - - // 그룹 내 예약어 여부 확인 - public static function isReservedInGroup(string $fieldKey, int $groupId): bool; -} -``` - -### ItemFieldService 검증 메서드 - -```php -private function validateFieldKeyUnique( - string $fieldKey, - int $tenantId, - ?string $sourceTable = null, - int $groupId = 1, - ?int $excludeId = null -): void { - // 1. 시스템 필드(예약어) 체크 - if ($sourceTable) { - if (SystemFields::isReserved($fieldKey, $sourceTable)) { - throw ValidationException::withMessages([ - 'field_key' => [__('error.field_key_reserved', ['field_key' => $fieldKey])], - ]); - } - } else { - // 안전 모드: 그룹 내 모든 테이블 예약어 체크 - if (SystemFields::isReservedInGroup($fieldKey, $groupId)) { - throw ValidationException::withMessages([ - 'field_key' => [__('error.field_key_reserved', ['field_key' => $fieldKey])], - ]); - } - } - - // 2. 기존 필드 중복 체크 - // ... -} -``` - -### 호출 예시 - -```php -// 독립 필드 생성 시 -$this->validateFieldKeyUnique( - $data['field_key'], - $tenantId, - $data['source_table'] ?? null, // 'products' 또는 'materials' - $data['group_id'] ?? 1 -); - -// 필드 수정 시 -$this->validateFieldKeyUnique( - $data['field_key'], - $tenantId, - $data['source_table'] ?? null, - $field->group_id ?? 1, - $id // excludeId -); -``` - -## 에러 메시지 - -| 상황 | 메시지 키 | 메시지 | -|------|----------|--------| -| 시스템 예약어 충돌 | `error.field_key_reserved` | `"code"은(는) 시스템 예약어로 사용할 수 없습니다.` | -| 기존 필드 중복 | `validation.unique` | `field_key은(는) 이미 사용 중입니다.` | - -```php -// lang/ko/error.php -'field_key_reserved' => '":field_key"은(는) 시스템 예약어로 사용할 수 없습니다.', - -// lang/ko/validation.php (Laravel 기본) -'unique' => ':attribute은(는) 이미 사용 중입니다.', -``` - -## clone 메서드 field_key 복제 정책 - -``` -원본 field_key: custom_field -복제본: custom_field_copy - -중복 시: custom_field_copy2, custom_field_copy3, ... -``` - -## 관련 파일 - -| 파일 | 변경 유형 | 설명 | -|------|----------|------| -| `app/Constants/SystemFields.php` | 신규 | 예약어 상수 클래스 | -| `app/Services/ItemMaster/ItemFieldService.php` | 수정 | 검증 로직 추가 | -| `lang/ko/error.php` | 수정 | 에러 메시지 추가 | - -## 참고 - -- ItemPage 테이블의 `source_table` 컬럼: 실제 저장 테이블명 (products, materials) -- ItemPage 테이블의 `item_type` 컬럼: FG, PT, SM, RM, CS (품목 유형 코드) -- `group_id`: 카테고리 격리용 (1 = 품목관리)