From 7109fc5199c80e23d75fec7efceb0b97f07b1da3 Mon Sep 17 00:00:00 2001 From: hskwon Date: Thu, 20 Nov 2025 16:36:55 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20ItemMaster=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EB=B2=A0=EC=9D=B4=EC=8A=A4=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EA=B5=AC=EC=B6=95=20(9=EA=B0=9C=20=ED=85=8C=EC=9D=B4=EB=B8=94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 마이그레이션 9개 생성 (unit_options, section_templates, item_master_fields, item_pages, item_sections, item_fields, item_bom_items, custom_tabs, tab_columns) - Eloquent 모델 9개 구현 (ItemMaster 네임스페이스) - ItemMasterSeeder 작성 및 테스트 데이터 생성 주요 특징: - Multi-tenant 지원 (BelongsToTenant trait) - Soft Delete 적용 (deleted_at, deleted_by) - 감사 로그 지원 (created_by, updated_by) - JSON 필드로 동적 속성 지원 (display_condition, validation_rules, options, properties) - FK 제약조건 및 Composite Index 설정 - 계층 구조 (ItemPage → ItemSection → ItemField/ItemBomItem) SAM API Development Rules 준수 --- CURRENT_WORKS.md | 289 ++++++++++++++++++ LOGICAL_RELATIONSHIPS.md | 34 ++- app/Models/ItemMaster/CustomTab.php | 45 +++ app/Models/ItemMaster/ItemBomItem.php | 51 ++++ app/Models/ItemMaster/ItemField.php | 56 ++++ app/Models/ItemMaster/ItemMasterField.php | 44 +++ app/Models/ItemMaster/ItemPage.php | 44 +++ app/Models/ItemMaster/ItemSection.php | 60 ++++ app/Models/ItemMaster/SectionTemplate.php | 36 +++ app/Models/ItemMaster/TabColumn.php | 34 +++ app/Models/ItemMaster/UnitOption.php | 33 ++ ...11_20_100000_create_unit_options_table.php | 42 +++ ..._100001_create_section_templates_table.php | 44 +++ ...100002_create_item_master_fields_table.php | 50 +++ ...5_11_20_100003_create_item_pages_table.php | 45 +++ ...1_20_100004_create_item_sections_table.php | 48 +++ ..._11_20_100005_create_item_fields_table.php | 55 ++++ ..._20_100006_create_item_bom_items_table.php | 52 ++++ ..._11_20_100007_create_custom_tabs_table.php | 45 +++ ..._11_20_100008_create_tab_columns_table.php | 43 +++ database/seeders/ItemMasterSeeder.php | 221 ++++++++++++++ 21 files changed, 1370 insertions(+), 1 deletion(-) create mode 100644 app/Models/ItemMaster/CustomTab.php create mode 100644 app/Models/ItemMaster/ItemBomItem.php create mode 100644 app/Models/ItemMaster/ItemField.php create mode 100644 app/Models/ItemMaster/ItemMasterField.php create mode 100644 app/Models/ItemMaster/ItemPage.php create mode 100644 app/Models/ItemMaster/ItemSection.php create mode 100644 app/Models/ItemMaster/SectionTemplate.php create mode 100644 app/Models/ItemMaster/TabColumn.php create mode 100644 app/Models/ItemMaster/UnitOption.php create mode 100644 database/migrations/2025_11_20_100000_create_unit_options_table.php create mode 100644 database/migrations/2025_11_20_100001_create_section_templates_table.php create mode 100644 database/migrations/2025_11_20_100002_create_item_master_fields_table.php create mode 100644 database/migrations/2025_11_20_100003_create_item_pages_table.php create mode 100644 database/migrations/2025_11_20_100004_create_item_sections_table.php create mode 100644 database/migrations/2025_11_20_100005_create_item_fields_table.php create mode 100644 database/migrations/2025_11_20_100006_create_item_bom_items_table.php create mode 100644 database/migrations/2025_11_20_100007_create_custom_tabs_table.php create mode 100644 database/migrations/2025_11_20_100008_create_tab_columns_table.php create mode 100644 database/seeders/ItemMasterSeeder.php diff --git a/CURRENT_WORKS.md b/CURRENT_WORKS.md index d58b089..8aab64b 100644 --- a/CURRENT_WORKS.md +++ b/CURRENT_WORKS.md @@ -1,3 +1,292 @@ +## 2025-11-20 (수) - ItemMaster 데이터베이스 구조 구축 + +### 주요 작업 +- ItemMaster 시스템을 위한 9개 테이블 마이그레이션 생성 및 실행 +- 9개 Eloquent 모델 클래스 구현 +- 시드 데이터 작성 및 테스트 데이터 생성 +- SAM API Development Rules 준수 (Multi-tenant, SoftDeletes, 감사 로그) + +### 추가된 파일 + +#### 마이그레이션 (9개) +1. **database/migrations/2025_11_20_100000_create_unit_options_table.php** + - 단위 옵션 테이블 (kg, EA, m, mm, L, BOX) + - tenant_id, label, value, created_by, updated_by, deleted_by + +2. **database/migrations/2025_11_20_100001_create_section_templates_table.php** + - 섹션 템플릿 테이블 (재사용 가능한 섹션 정의) + - title, type (fields/bom), description, is_default + +3. **database/migrations/2025_11_20_100002_create_item_master_fields_table.php** + - 마스터 필드 라이브러리 + - field_name, field_type, category, is_common + - options, validation_rules, properties (JSON) + +4. **database/migrations/2025_11_20_100003_create_item_pages_table.php** + - 품목 페이지 테이블 (FG/PT/SM/RM/CS) + - page_name, item_type (ENUM), absolute_path, is_active + +5. **database/migrations/2025_11_20_100004_create_item_sections_table.php** + - 섹션 인스턴스 테이블 + - page_id (FK), title, type, order_no + - Composite Index: (page_id, order_no) + +6. **database/migrations/2025_11_20_100005_create_item_fields_table.php** + - 필드 인스턴스 테이블 + - section_id (FK), field_name, field_type, order_no, is_required + - display_condition, validation_rules, options, properties (JSON) + - Composite Index: (section_id, order_no) + +7. **database/migrations/2025_11_20_100006_create_item_bom_items_table.php** + - BOM 항목 테이블 + - section_id (FK), item_code, item_name, quantity, unit + - unit_price, total_price, spec, note + +8. **database/migrations/2025_11_20_100007_create_custom_tabs_table.php** + - 커스텀 탭 정의 + - label, icon, is_default, order_no + +9. **database/migrations/2025_11_20_100008_create_tab_columns_table.php** + - 탭별 컬럼 설정 + - tab_id (FK), columns (JSON) + - Unique: (tenant_id, tab_id) + +#### 모델 (9개) +1. **app/Models/ItemMaster/UnitOption.php** + - Traits: BelongsToTenant, ModelTrait, SoftDeletes + +2. **app/Models/ItemMaster/SectionTemplate.php** + - is_default boolean cast + +3. **app/Models/ItemMaster/ItemMasterField.php** + - options, validation_rules, properties array cast + +4. **app/Models/ItemMaster/ItemPage.php** + - Relationship: sections() hasMany + - is_active boolean cast + +5. **app/Models/ItemMaster/ItemSection.php** + - Relationships: page() belongsTo, fields() hasMany, bomItems() hasMany + - order_no integer cast + +6. **app/Models/ItemMaster/ItemField.php** + - Relationship: section() belongsTo + - 4개 JSON 필드 array cast + +7. **app/Models/ItemMaster/ItemBomItem.php** + - Relationship: section() belongsTo + - quantity decimal:4, prices decimal:2 + +8. **app/Models/ItemMaster/CustomTab.php** + - Relationship: columnSetting() hasOne + - is_default boolean, order_no integer cast + +9. **app/Models/ItemMaster/TabColumn.php** + - Relationship: tab() belongsTo + - columns array cast + - SoftDeletes 미적용 (설정 데이터) + +#### 시더 +1. **database/seeders/ItemMasterSeeder.php** + - 6개 단위 옵션 생성 + - 3개 섹션 템플릿 생성 + - 8개 마스터 필드 생성 + - 5개 품목 페이지 생성 (FG, PT, SM, RM, CS) + - 각 페이지에 기본 정보 섹션 + 필드 3개 + - FG/PT 페이지에 BOM 구성 섹션 + 샘플 BOM 항목 + - 3개 커스텀 탭 + 컬럼 설정 + +### 작업 내용 + +#### 1단계: 분석 +- 프론트엔드 API 명세서 분석 +- 기존 DB 구조와 요구사항 비교 +- 9개 테이블 간 의존성 파악 +- 마이그레이션 실행 순서 결정 + +#### 2단계: 수정 +- SAM API Development Rules 준수하여 마이그레이션 작성 + - BelongsToTenant, SoftDeletes 적용 + - created_by, updated_by, deleted_by 컬럼 (COMMENT 포함) + - FK 제약조건 with ON DELETE CASCADE +- Eloquent 모델 작성 + - 관계 설정 (hasMany, belongsTo, hasOne) + - JSON 필드 캐스팅 + - ModelTrait 적용 +- 시더 작성 (실제 사용 가능한 테스트 데이터) + +#### 3단계: 검증 +- `php artisan migrate --pretend` 실행 → 순서 확인 +- `php artisan migrate` 실행 → 9개 테이블 생성 성공 +- `php artisan db:seed --class=ItemMasterSeeder` 실행 +- 데이터 검증: + - Unit Options: 6개 + - Item Pages: 5개 + - Item Sections: 7개 + - Item Fields: 15개 + - BOM Items: 2개 + +#### 4단계: 정리 +- 임시 파일 확인 → 없음 + +### 기술적 특징 + +#### Multi-tenant 지원 +- 모든 테이블에 tenant_id 컬럼 +- BelongsToTenant trait로 글로벌 스코프 자동 적용 +- 테넌트 격리 보장 + +#### 감사 로그 지원 +- created_by, updated_by, deleted_by 컬럼 +- 모든 변경사항 추적 가능 + +#### 유연한 스키마 +- JSON 필드로 동적 속성 지원 + - display_condition: 조건부 표시 + - validation_rules: 유효성 검사 규칙 + - options: 드롭다운 옵션 + - properties: 필드 속성 (unit, precision, format) + +#### 정렬 및 순서 관리 +- order_no 컬럼으로 UI 순서 제어 +- Composite Index로 빠른 정렬 쿼리 + +#### 관계 설정 +- ItemPage → ItemSection → ItemField (계층 구조) +- ItemPage → ItemSection → ItemBomItem (BOM 구조) +- CustomTab → TabColumn (1:1 관계) + +### 다음 단계 (Week 2-3) +- ItemPageController, ItemSectionController 구현 +- ItemFieldController, ItemBomItemController 구현 +- FormRequest 클래스 작성 +- Service 클래스 구현 (비즈니스 로직) +- Swagger 문서 작성 (/api/v1/item-master/*) + +### Git 커밋 +```bash +# 커밋 예정 +``` + +--- + +## 2025-11-17 (일) - is_active 컬럼 추가 (products, materials 테이블) + +### 주요 작업 +- products 및 materials 테이블에 is_active 컬럼 추가 +- Product 및 Material 모델 업데이트 +- ModelTrait의 scopeActive() 메서드 지원 + +### 문제 상황 +- GET /api/v1/items 실행 시 `SQLSTATE[42S22]: Column not found: 1054 Unknown column 'is_active'` 에러 발생 +- ItemsService에서 `->where('is_active', 1)` 조건 사용 +- ModelTrait에 `scopeActive()` 메서드가 is_active 컬럼 참조 +- products, materials 테이블에 is_active 컬럼 없음 + +### 추가된 파일 +1. **database/migrations/2025_11_17_145307_add_is_active_to_products_and_materials_tables.php** + - products 테이블에 is_active 추가 (tinyInteger, default 1, AFTER updated_by) + - materials 테이블에 is_active 추가 (tinyInteger, default 1, AFTER updated_by) + - 롤백 지원 (down 메서드) + +### 수정된 파일 +1. **app/Models/Products/Product.php** + - fillable에 'is_active' 추가 + - casts에 'is_active' => 'boolean' 추가 + +2. **app/Models/Materials/Material.php** + - fillable에 'material_type', 'is_active' 추가 + - casts에 'is_active' => 'boolean' 추가 + +### 작업 내용 +- ModelTrait의 scopeActive() 메서드가 정상 작동하도록 is_active 컬럼 추가 +- 기본값 1로 설정하여 기존 데이터는 활성 상태로 유지 +- Material 모델에 누락된 material_type도 fillable에 추가 + +### Git 커밋 +```bash +git commit 517d594 +feat: products 및 materials 테이블에 is_active 컬럼 추가 + +- is_active 컬럼 추가 마이그레이션 (default 1) +- Product 모델 fillable 및 casts 업데이트 +- Material 모델 fillable 및 casts 업데이트 +- Material 모델에 material_type fillable 추가 +- ModelTrait의 scopeActive() 메서드 지원 + +git commit 8ce8a35 +fix: is_active 마이그레이션에 컬럼 존재 여부 체크 추가 + +- Schema::hasColumn()으로 컬럼 존재 여부 확인 +- 개발 서버와 로컬 환경의 스키마 차이 대응 +- 중복 컬럼 추가 오류 방지 +``` + +### 환경별 차이 발견 +- **개발 서버**: is_active 컬럼이 이미 존재함 +- **로컬 환경**: is_active 컬럼이 없었음 +- **원인**: 환경 간 DB 스키마 동기화가 안 되어 있었음 +- **해결**: 컬럼 존재 여부를 체크하는 로직 추가로 양쪽 환경 모두 대응 + +### 개발 서버 마이그레이션 처리 방법 +```bash +# 방법 1: 롤백 후 재실행 +php artisan migrate:rollback --step=1 +git pull origin develop +php artisan migrate + +# 방법 2: 실패한 마이그레이션 레코드만 정리 (is_active가 이미 있으므로) +# migrations 테이블에서 해당 레코드를 수동으로 완료 처리 +``` + +### 검증 완료 +- [x] 마이그레이션 실행 성공 (로컬) +- [x] Product 모델 업데이트 +- [x] Material 모델 업데이트 +- [x] 컬럼 존재 여부 체크 추가 +- [x] Pint 포맷팅 통과 +- [x] Git 커밋 완료 (2개) + +--- + +## 2025-11-17 (일) - BP-MES Phase 1: Swagger 문서 보완 (GET /items 엔드포인트) + +### 주요 작업 +- ItemsApi Swagger 문서 누락 부분 추가 +- GET /api/v1/items 엔드포인트 문서화 완료 + +### 수정된 파일 +1. **app/Swagger/v1/ItemsApi.php** + - index() 메서드 Swagger 문서 추가 + - 품목 목록 조회 (통합) 엔드포인트 문서화 + - 페이징 파라미터 (page, size) 정의 + - 검색 및 필터 파라미터 정의 (search, product_type, category_id, is_active) + - 응답 스키마 정의 (Item array + PaginationMeta) + +### 작업 내용 +- 라우트는 존재했지만 Swagger 문서가 누락되어 있던 GET /items 엔드포인트 문서화 +- ItemsApi.php의 5개 엔드포인트 중 index()만 문서가 없었던 문제 해결 +- l5-swagger:generate 실행으로 Swagger JSON 재생성 + +### Git 커밋 +```bash +git commit 7b8f879 +docs: GET /items 엔드포인트 Swagger 문서 추가 + +- ItemsApi.php에 index() 메서드 문서 추가 +- 품목 목록 조회 (통합) 엔드포인트 문서화 +- 페이징, 검색, 필터 파라미터 정의 +- Swagger JSON 재생성 완료 +``` + +### 검증 완료 +- [x] Swagger 문서 추가 (index 메서드) +- [x] Swagger JSON 재생성 +- [x] Pint 포맷팅 통과 +- [x] Git 커밋 완료 + +--- + ## 2025-11-17 (일) - BP-MES Phase 1: Items BOM API 및 File Upload API 구현 완료 ### 주요 작업 diff --git a/LOGICAL_RELATIONSHIPS.md b/LOGICAL_RELATIONSHIPS.md index 983a250..ac6d742 100644 --- a/LOGICAL_RELATIONSHIPS.md +++ b/LOGICAL_RELATIONSHIPS.md @@ -1,6 +1,6 @@ # 논리적 데이터베이스 관계 문서 -> **자동 생성**: 2025-11-17 13:00:34 +> **자동 생성**: 2025-11-20 16:28:56 > **소스**: Eloquent 모델 관계 분석 ## 📊 모델별 관계 현황 @@ -123,6 +123,38 @@ ### folders **모델**: `App\Models\Folder` +### custom_tabs +**모델**: `App\Models\ItemMaster\CustomTab` + +- **columnSetting()**: hasOne → `tab_columns` + +### item_bom_items +**모델**: `App\Models\ItemMaster\ItemBomItem` + +- **section()**: belongsTo → `item_sections` + +### item_fields +**모델**: `App\Models\ItemMaster\ItemField` + +- **section()**: belongsTo → `item_sections` + +### item_pages +**모델**: `App\Models\ItemMaster\ItemPage` + +- **sections()**: hasMany → `item_sections` + +### item_sections +**모델**: `App\Models\ItemMaster\ItemSection` + +- **page()**: belongsTo → `item_pages` +- **fields()**: hasMany → `item_fields` +- **bomItems()**: hasMany → `item_bom_items` + +### tab_columns +**모델**: `App\Models\ItemMaster\TabColumn` + +- **tab()**: belongsTo → `custom_tabs` + ### main_requests **모델**: `App\Models\MainRequest` diff --git a/app/Models/ItemMaster/CustomTab.php b/app/Models/ItemMaster/CustomTab.php new file mode 100644 index 0000000..fd9d3b4 --- /dev/null +++ b/app/Models/ItemMaster/CustomTab.php @@ -0,0 +1,45 @@ + 'boolean', + 'order_no' => 'integer', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; + + protected $hidden = [ + 'deleted_by', + 'deleted_at', + ]; + + /** + * 탭의 컬럼 설정 + */ + public function columnSetting() + { + return $this->hasOne(TabColumn::class, 'tab_id'); + } +} diff --git a/app/Models/ItemMaster/ItemBomItem.php b/app/Models/ItemMaster/ItemBomItem.php new file mode 100644 index 0000000..271c404 --- /dev/null +++ b/app/Models/ItemMaster/ItemBomItem.php @@ -0,0 +1,51 @@ + 'decimal:4', + 'unit_price' => 'decimal:2', + 'total_price' => 'decimal:2', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; + + protected $hidden = [ + 'deleted_by', + 'deleted_at', + ]; + + /** + * 소속 섹션 + */ + public function section() + { + return $this->belongsTo(ItemSection::class, 'section_id'); + } +} diff --git a/app/Models/ItemMaster/ItemField.php b/app/Models/ItemMaster/ItemField.php new file mode 100644 index 0000000..90c4791 --- /dev/null +++ b/app/Models/ItemMaster/ItemField.php @@ -0,0 +1,56 @@ + 'integer', + 'is_required' => 'boolean', + 'display_condition' => 'array', + 'validation_rules' => 'array', + 'options' => 'array', + 'properties' => 'array', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; + + protected $hidden = [ + 'deleted_by', + 'deleted_at', + ]; + + /** + * 소속 섹션 + */ + public function section() + { + return $this->belongsTo(ItemSection::class, 'section_id'); + } +} diff --git a/app/Models/ItemMaster/ItemMasterField.php b/app/Models/ItemMaster/ItemMasterField.php new file mode 100644 index 0000000..b2b1c55 --- /dev/null +++ b/app/Models/ItemMaster/ItemMasterField.php @@ -0,0 +1,44 @@ + 'boolean', + 'options' => 'array', + 'validation_rules' => 'array', + 'properties' => 'array', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; + + protected $hidden = [ + 'deleted_by', + 'deleted_at', + ]; +} \ No newline at end of file diff --git a/app/Models/ItemMaster/ItemPage.php b/app/Models/ItemMaster/ItemPage.php new file mode 100644 index 0000000..edc11be --- /dev/null +++ b/app/Models/ItemMaster/ItemPage.php @@ -0,0 +1,44 @@ + 'boolean', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; + + protected $hidden = [ + 'deleted_by', + 'deleted_at', + ]; + + /** + * 페이지의 섹션 목록 + */ + public function sections() + { + return $this->hasMany(ItemSection::class, 'page_id')->orderBy('order_no'); + } +} \ No newline at end of file diff --git a/app/Models/ItemMaster/ItemSection.php b/app/Models/ItemMaster/ItemSection.php new file mode 100644 index 0000000..269327d --- /dev/null +++ b/app/Models/ItemMaster/ItemSection.php @@ -0,0 +1,60 @@ + 'integer', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; + + protected $hidden = [ + 'deleted_by', + 'deleted_at', + ]; + + /** + * 소속 페이지 + */ + public function page() + { + return $this->belongsTo(ItemPage::class, 'page_id'); + } + + /** + * 섹션의 필드 목록 + */ + public function fields() + { + return $this->hasMany(ItemField::class, 'section_id')->orderBy('order_no'); + } + + /** + * 섹션의 BOM 항목 목록 + */ + public function bomItems() + { + return $this->hasMany(ItemBomItem::class, 'section_id'); + } +} diff --git a/app/Models/ItemMaster/SectionTemplate.php b/app/Models/ItemMaster/SectionTemplate.php new file mode 100644 index 0000000..4ddd4c7 --- /dev/null +++ b/app/Models/ItemMaster/SectionTemplate.php @@ -0,0 +1,36 @@ + 'boolean', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; + + protected $hidden = [ + 'deleted_by', + 'deleted_at', + ]; +} \ No newline at end of file diff --git a/app/Models/ItemMaster/TabColumn.php b/app/Models/ItemMaster/TabColumn.php new file mode 100644 index 0000000..68bde18 --- /dev/null +++ b/app/Models/ItemMaster/TabColumn.php @@ -0,0 +1,34 @@ + 'array', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + /** + * 소속 탭 + */ + public function tab() + { + return $this->belongsTo(CustomTab::class, 'tab_id'); + } +} diff --git a/app/Models/ItemMaster/UnitOption.php b/app/Models/ItemMaster/UnitOption.php new file mode 100644 index 0000000..a42ef58 --- /dev/null +++ b/app/Models/ItemMaster/UnitOption.php @@ -0,0 +1,33 @@ + 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; + + protected $hidden = [ + 'deleted_by', + 'deleted_at', + ]; +} \ No newline at end of file diff --git a/database/migrations/2025_11_20_100000_create_unit_options_table.php b/database/migrations/2025_11_20_100000_create_unit_options_table.php new file mode 100644 index 0000000..3a5100c --- /dev/null +++ b/database/migrations/2025_11_20_100000_create_unit_options_table.php @@ -0,0 +1,42 @@ +id()->comment('단위 옵션 ID'); + $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); + $table->string('label', 100)->comment('단위 라벨'); + $table->string('value', 50)->comment('단위 값'); + $table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID'); + $table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID'); + $table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자 ID'); + $table->timestamps(); + $table->softDeletes()->comment('소프트 삭제'); + + // 인덱스 + $table->index('tenant_id', 'idx_unit_options_tenant_id'); + + // 외래키 + $table->foreign('tenant_id', 'fk_unit_options_tenant') + ->references('id')->on('tenants') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('unit_options'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_11_20_100001_create_section_templates_table.php b/database/migrations/2025_11_20_100001_create_section_templates_table.php new file mode 100644 index 0000000..80ca314 --- /dev/null +++ b/database/migrations/2025_11_20_100001_create_section_templates_table.php @@ -0,0 +1,44 @@ +id()->comment('섹션 템플릿 ID'); + $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); + $table->string('title')->comment('템플릿명'); + $table->enum('type', ['fields', 'bom'])->default('fields')->comment('섹션 타입 (fields: 필드형, bom: BOM형)'); + $table->text('description')->nullable()->comment('설명'); + $table->boolean('is_default')->default(false)->comment('기본 템플릿 여부'); + $table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID'); + $table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID'); + $table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자 ID'); + $table->timestamps(); + $table->softDeletes()->comment('소프트 삭제'); + + // 인덱스 + $table->index('tenant_id', 'idx_section_templates_tenant_id'); + + // 외래키 + $table->foreign('tenant_id', 'fk_section_templates_tenant') + ->references('id')->on('tenants') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('section_templates'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_11_20_100002_create_item_master_fields_table.php b/database/migrations/2025_11_20_100002_create_item_master_fields_table.php new file mode 100644 index 0000000..1a9d9ec --- /dev/null +++ b/database/migrations/2025_11_20_100002_create_item_master_fields_table.php @@ -0,0 +1,50 @@ +id()->comment('마스터 필드 ID'); + $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); + $table->string('field_name')->comment('필드명'); + $table->enum('field_type', ['textbox', 'number', 'dropdown', 'checkbox', 'date', 'textarea'])->comment('필드 타입'); + $table->string('category', 100)->nullable()->comment('카테고리'); + $table->text('description')->nullable()->comment('설명'); + $table->boolean('is_common')->default(false)->comment('공통 필드 여부'); + $table->text('default_value')->nullable()->comment('기본값'); + $table->json('options')->nullable()->comment('드롭다운 옵션 [{"label": "옵션1", "value": "val1"}]'); + $table->json('validation_rules')->nullable()->comment('검증 규칙 {"min": 0, "max": 100, "pattern": "regex"}'); + $table->json('properties')->nullable()->comment('필드 속성 {"unit": "mm", "precision": 2, "format": "YYYY-MM-DD"}'); + $table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID'); + $table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID'); + $table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자 ID'); + $table->timestamps(); + $table->softDeletes()->comment('소프트 삭제'); + + // 인덱스 + $table->index('tenant_id', 'idx_item_master_fields_tenant_id'); + $table->index('category', 'idx_item_master_fields_category'); + + // 외래키 + $table->foreign('tenant_id', 'fk_item_master_fields_tenant') + ->references('id')->on('tenants') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('item_master_fields'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_11_20_100003_create_item_pages_table.php b/database/migrations/2025_11_20_100003_create_item_pages_table.php new file mode 100644 index 0000000..34a412f --- /dev/null +++ b/database/migrations/2025_11_20_100003_create_item_pages_table.php @@ -0,0 +1,45 @@ +id()->comment('품목 페이지 ID'); + $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); + $table->string('page_name')->comment('페이지명'); + $table->enum('item_type', ['FG', 'PT', 'SM', 'RM', 'CS'])->comment('품목 유형 (FG: 완제품, PT: 반제품, SM: 부자재, RM: 원자재, CS: 소모품)'); + $table->string('absolute_path', 500)->nullable()->comment('절대 경로'); + $table->boolean('is_active')->default(true)->comment('활성 여부'); + $table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID'); + $table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID'); + $table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자 ID'); + $table->timestamps(); + $table->softDeletes()->comment('소프트 삭제'); + + // 인덱스 + $table->index('tenant_id', 'idx_item_pages_tenant_id'); + $table->index('item_type', 'idx_item_pages_item_type'); + + // 외래키 + $table->foreign('tenant_id', 'fk_item_pages_tenant') + ->references('id')->on('tenants') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('item_pages'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_11_20_100004_create_item_sections_table.php b/database/migrations/2025_11_20_100004_create_item_sections_table.php new file mode 100644 index 0000000..6f9e424 --- /dev/null +++ b/database/migrations/2025_11_20_100004_create_item_sections_table.php @@ -0,0 +1,48 @@ +id()->comment('섹션 인스턴스 ID'); + $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); + $table->unsignedBigInteger('page_id')->comment('페이지 ID'); + $table->string('title')->comment('섹션 제목'); + $table->enum('type', ['fields', 'bom'])->default('fields')->comment('섹션 타입 (fields: 필드형, bom: BOM형)'); + $table->integer('order_no')->default(0)->comment('정렬 순서'); + $table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID'); + $table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID'); + $table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자 ID'); + $table->timestamps(); + $table->softDeletes()->comment('소프트 삭제'); + + // 인덱스 + $table->index(['tenant_id', 'page_id'], 'idx_item_sections_tenant_page'); + $table->index(['page_id', 'order_no'], 'idx_item_sections_order'); + + // 외래키 + $table->foreign('tenant_id', 'fk_item_sections_tenant') + ->references('id')->on('tenants') + ->onDelete('cascade'); + $table->foreign('page_id', 'fk_item_sections_page') + ->references('id')->on('item_pages') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('item_sections'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_11_20_100005_create_item_fields_table.php b/database/migrations/2025_11_20_100005_create_item_fields_table.php new file mode 100644 index 0000000..39d660e --- /dev/null +++ b/database/migrations/2025_11_20_100005_create_item_fields_table.php @@ -0,0 +1,55 @@ +id()->comment('필드 ID'); + $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); + $table->unsignedBigInteger('section_id')->comment('섹션 ID'); + $table->string('field_name')->comment('필드명'); + $table->enum('field_type', ['textbox', 'number', 'dropdown', 'checkbox', 'date', 'textarea'])->comment('필드 타입'); + $table->integer('order_no')->default(0)->comment('정렬 순서'); + $table->boolean('is_required')->default(false)->comment('필수 여부'); + $table->text('default_value')->nullable()->comment('기본값'); + $table->string('placeholder')->nullable()->comment('플레이스홀더'); + $table->json('display_condition')->nullable()->comment('표시 조건 {"field_id": "1", "operator": "equals", "value": "true"}'); + $table->json('validation_rules')->nullable()->comment('검증 규칙 {"min": 0, "max": 100, "pattern": "regex"}'); + $table->json('options')->nullable()->comment('드롭다운 옵션 [{"label": "옵션1", "value": "val1"}]'); + $table->json('properties')->nullable()->comment('필드 속성 {"unit": "mm", "precision": 2, "format": "YYYY-MM-DD"}'); + $table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID'); + $table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID'); + $table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자 ID'); + $table->timestamps(); + $table->softDeletes()->comment('소프트 삭제'); + + // 인덱스 + $table->index(['tenant_id', 'section_id'], 'idx_item_fields_tenant_section'); + $table->index(['section_id', 'order_no'], 'idx_item_fields_order'); + + // 외래키 + $table->foreign('tenant_id', 'fk_item_fields_tenant') + ->references('id')->on('tenants') + ->onDelete('cascade'); + $table->foreign('section_id', 'fk_item_fields_section') + ->references('id')->on('item_sections') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('item_fields'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_11_20_100006_create_item_bom_items_table.php b/database/migrations/2025_11_20_100006_create_item_bom_items_table.php new file mode 100644 index 0000000..a9a128b --- /dev/null +++ b/database/migrations/2025_11_20_100006_create_item_bom_items_table.php @@ -0,0 +1,52 @@ +id()->comment('BOM 항목 ID'); + $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); + $table->unsignedBigInteger('section_id')->comment('섹션 ID'); + $table->string('item_code', 100)->nullable()->comment('품목 코드'); + $table->string('item_name')->comment('품목명'); + $table->decimal('quantity', 15, 4)->default(0)->comment('수량'); + $table->string('unit', 50)->nullable()->comment('단위'); + $table->decimal('unit_price', 15, 2)->nullable()->comment('단가'); + $table->decimal('total_price', 15, 2)->nullable()->comment('총액'); + $table->text('spec')->nullable()->comment('사양'); + $table->text('note')->nullable()->comment('비고'); + $table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID'); + $table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID'); + $table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자 ID'); + $table->timestamps(); + $table->softDeletes()->comment('소프트 삭제'); + + // 인덱스 + $table->index(['tenant_id', 'section_id'], 'idx_item_bom_items_tenant_section'); + + // 외래키 + $table->foreign('tenant_id', 'fk_item_bom_items_tenant') + ->references('id')->on('tenants') + ->onDelete('cascade'); + $table->foreign('section_id', 'fk_item_bom_items_section') + ->references('id')->on('item_sections') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('item_bom_items'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_11_20_100007_create_custom_tabs_table.php b/database/migrations/2025_11_20_100007_create_custom_tabs_table.php new file mode 100644 index 0000000..f2d5da6 --- /dev/null +++ b/database/migrations/2025_11_20_100007_create_custom_tabs_table.php @@ -0,0 +1,45 @@ +id()->comment('커스텀 탭 ID'); + $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); + $table->string('label')->comment('탭 라벨'); + $table->string('icon', 100)->nullable()->comment('아이콘'); + $table->boolean('is_default')->default(false)->comment('기본 탭 여부'); + $table->integer('order_no')->default(0)->comment('정렬 순서'); + $table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID'); + $table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID'); + $table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자 ID'); + $table->timestamps(); + $table->softDeletes()->comment('소프트 삭제'); + + // 인덱스 + $table->index('tenant_id', 'idx_custom_tabs_tenant_id'); + $table->index(['tenant_id', 'order_no'], 'idx_custom_tabs_order'); + + // 외래키 + $table->foreign('tenant_id', 'fk_custom_tabs_tenant') + ->references('id')->on('tenants') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('custom_tabs'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_11_20_100008_create_tab_columns_table.php b/database/migrations/2025_11_20_100008_create_tab_columns_table.php new file mode 100644 index 0000000..e7f28ba --- /dev/null +++ b/database/migrations/2025_11_20_100008_create_tab_columns_table.php @@ -0,0 +1,43 @@ +id()->comment('탭 컬럼 설정 ID'); + $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); + $table->unsignedBigInteger('tab_id')->comment('탭 ID'); + $table->json('columns')->comment('컬럼 설정 [{"key": "name", "label": "품목명", "visible": true, "order": 0}]'); + $table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID'); + $table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID'); + $table->timestamps(); + + // 인덱스 + $table->unique(['tenant_id', 'tab_id'], 'uk_tab_columns_tenant_tab'); + + // 외래키 + $table->foreign('tenant_id', 'fk_tab_columns_tenant') + ->references('id')->on('tenants') + ->onDelete('cascade'); + $table->foreign('tab_id', 'fk_tab_columns_tab') + ->references('id')->on('custom_tabs') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('tab_columns'); + } +}; \ No newline at end of file diff --git a/database/seeders/ItemMasterSeeder.php b/database/seeders/ItemMasterSeeder.php new file mode 100644 index 0000000..5296f1f --- /dev/null +++ b/database/seeders/ItemMasterSeeder.php @@ -0,0 +1,221 @@ + '킬로그램', 'value' => 'kg'], + ['label' => '개', 'value' => 'EA'], + ['label' => '미터', 'value' => 'm'], + ['label' => '밀리미터', 'value' => 'mm'], + ['label' => '리터', 'value' => 'L'], + ['label' => '박스', 'value' => 'BOX'], + ]; + + foreach ($units as $unit) { + UnitOption::create([ + 'tenant_id' => $tenantId, + 'label' => $unit['label'], + 'value' => $unit['value'], + 'created_by' => $userId, + ]); + } + + // 2. 섹션 템플릿 + $templates = [ + ['title' => '기본 정보', 'type' => 'fields', 'description' => '제품 기본 정보 입력용 템플릿', 'is_default' => true], + ['title' => '치수 정보', 'type' => 'fields', 'description' => '제품 치수 정보 입력용 템플릿', 'is_default' => false], + ['title' => 'BOM 구성', 'type' => 'bom', 'description' => 'BOM 항목 관리용 템플릿', 'is_default' => true], + ]; + + foreach ($templates as $template) { + SectionTemplate::create([ + 'tenant_id' => $tenantId, + 'title' => $template['title'], + 'type' => $template['type'], + 'description' => $template['description'], + 'is_default' => $template['is_default'], + 'created_by' => $userId, + ]); + } + + // 3. 마스터 필드 + $masterFields = [ + ['field_name' => '제품명', 'field_type' => 'textbox', 'category' => '기본정보', 'is_common' => true], + ['field_name' => '제품코드', 'field_type' => 'textbox', 'category' => '기본정보', 'is_common' => true], + ['field_name' => '규격', 'field_type' => 'textbox', 'category' => '기본정보', 'is_common' => true], + ['field_name' => '수량', 'field_type' => 'number', 'category' => '재고정보', 'is_common' => true], + ['field_name' => '단위', 'field_type' => 'dropdown', 'category' => '재고정보', 'is_common' => true, 'options' => [ + ['label' => 'kg', 'value' => 'kg'], + ['label' => 'EA', 'value' => 'EA'], + ['label' => 'm', 'value' => 'm'], + ]], + ['field_name' => '길이', 'field_type' => 'number', 'category' => '치수정보', 'is_common' => false, 'properties' => ['unit' => 'mm', 'precision' => 2]], + ['field_name' => '폭', 'field_type' => 'number', 'category' => '치수정보', 'is_common' => false, 'properties' => ['unit' => 'mm', 'precision' => 2]], + ['field_name' => '높이', 'field_type' => 'number', 'category' => '치수정보', 'is_common' => false, 'properties' => ['unit' => 'mm', 'precision' => 2]], + ]; + + foreach ($masterFields as $field) { + ItemMasterField::create([ + 'tenant_id' => $tenantId, + 'field_name' => $field['field_name'], + 'field_type' => $field['field_type'], + 'category' => $field['category'], + 'is_common' => $field['is_common'], + 'options' => $field['options'] ?? null, + 'properties' => $field['properties'] ?? null, + 'created_by' => $userId, + ]); + } + + // 4. 품목 페이지 (5개 유형) + $pages = [ + ['page_name' => '완제품 관리', 'item_type' => 'FG', 'absolute_path' => '/FG/완제품 관리'], + ['page_name' => '반제품 관리', 'item_type' => 'PT', 'absolute_path' => '/PT/반제품 관리'], + ['page_name' => '부자재 관리', 'item_type' => 'SM', 'absolute_path' => '/SM/부자재 관리'], + ['page_name' => '원자재 관리', 'item_type' => 'RM', 'absolute_path' => '/RM/원자재 관리'], + ['page_name' => '소모품 관리', 'item_type' => 'CS', 'absolute_path' => '/CS/소모품 관리'], + ]; + + foreach ($pages as $index => $page) { + $itemPage = ItemPage::create([ + 'tenant_id' => $tenantId, + 'page_name' => $page['page_name'], + 'item_type' => $page['item_type'], + 'absolute_path' => $page['absolute_path'], + 'is_active' => true, + 'created_by' => $userId, + ]); + + // 각 페이지에 기본 섹션 추가 + $section1 = ItemSection::create([ + 'tenant_id' => $tenantId, + 'page_id' => $itemPage->id, + 'title' => '기본 정보', + 'type' => 'fields', + 'order_no' => 0, + 'created_by' => $userId, + ]); + + // 섹션에 필드 추가 + ItemField::create([ + 'tenant_id' => $tenantId, + 'section_id' => $section1->id, + 'field_name' => '제품명', + 'field_type' => 'textbox', + 'order_no' => 0, + 'is_required' => true, + 'placeholder' => '제품명을 입력하세요', + 'created_by' => $userId, + ]); + + ItemField::create([ + 'tenant_id' => $tenantId, + 'section_id' => $section1->id, + 'field_name' => '제품코드', + 'field_type' => 'textbox', + 'order_no' => 1, + 'is_required' => true, + 'placeholder' => '제품코드를 입력하세요', + 'created_by' => $userId, + ]); + + ItemField::create([ + 'tenant_id' => $tenantId, + 'section_id' => $section1->id, + 'field_name' => '규격', + 'field_type' => 'textbox', + 'order_no' => 2, + 'is_required' => false, + 'placeholder' => '규격을 입력하세요', + 'created_by' => $userId, + ]); + + // BOM 섹션 (완제품, 반제품만) + if (in_array($page['item_type'], ['FG', 'PT'])) { + $section2 = ItemSection::create([ + 'tenant_id' => $tenantId, + 'page_id' => $itemPage->id, + 'title' => 'BOM 구성', + 'type' => 'bom', + 'order_no' => 1, + 'created_by' => $userId, + ]); + + // BOM 항목 샘플 + ItemBomItem::create([ + 'tenant_id' => $tenantId, + 'section_id' => $section2->id, + 'item_code' => 'MAT-001', + 'item_name' => '철판', + 'quantity' => 10.5, + 'unit' => 'kg', + 'unit_price' => 5000.00, + 'total_price' => 52500.00, + 'spec' => '두께 2mm, 스테인리스', + 'note' => '샘플 BOM 항목', + 'created_by' => $userId, + ]); + } + } + + // 5. 커스텀 탭 + $tabs = [ + ['label' => '전체', 'icon' => 'all', 'is_default' => true, 'order_no' => 0], + ['label' => '완제품', 'icon' => 'product', 'is_default' => false, 'order_no' => 1], + ['label' => '원자재', 'icon' => 'material', 'is_default' => false, 'order_no' => 2], + ]; + + foreach ($tabs as $tab) { + $customTab = CustomTab::create([ + 'tenant_id' => $tenantId, + 'label' => $tab['label'], + 'icon' => $tab['icon'], + 'is_default' => $tab['is_default'], + 'order_no' => $tab['order_no'], + 'created_by' => $userId, + ]); + + // 탭별 컬럼 설정 + TabColumn::create([ + 'tenant_id' => $tenantId, + 'tab_id' => $customTab->id, + 'columns' => [ + ['key' => 'name', 'label' => '품목명', 'visible' => true, 'order' => 0], + ['key' => 'code', 'label' => '품목코드', 'visible' => true, 'order' => 1], + ['key' => 'type', 'label' => '유형', 'visible' => true, 'order' => 2], + ['key' => 'price', 'label' => '가격', 'visible' => false, 'order' => 3], + ], + 'created_by' => $userId, + ]); + } + + echo "✅ ItemMaster 시드 데이터 생성 완료!\n"; + echo " - 단위 옵션: ".count($units)."개\n"; + echo " - 섹션 템플릿: ".count($templates)."개\n"; + echo " - 마스터 필드: ".count($masterFields)."개\n"; + echo " - 품목 페이지: ".count($pages)."개\n"; + echo " - 커스텀 탭: ".count($tabs)."개\n"; + } +}