feat: ItemMaster 데이터베이스 구조 구축 (9개 테이블)

- 마이그레이션 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 준수
This commit is contained in:
2025-11-20 16:36:55 +09:00
parent 8ce8a35f30
commit 7109fc5199
21 changed files with 1370 additions and 1 deletions

View File

@@ -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 구현 완료
### 주요 작업

View File

@@ -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`

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Models\ItemMaster;
use App\Traits\BelongsToTenant;
use App\Traits\ModelTrait;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class CustomTab extends Model
{
use BelongsToTenant, ModelTrait, SoftDeletes;
protected $fillable = [
'tenant_id',
'label',
'icon',
'is_default',
'order_no',
'created_by',
'updated_by',
'deleted_by',
];
protected $casts = [
'is_default' => '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');
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Models\ItemMaster;
use App\Traits\BelongsToTenant;
use App\Traits\ModelTrait;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class ItemBomItem extends Model
{
use BelongsToTenant, ModelTrait, SoftDeletes;
protected $fillable = [
'tenant_id',
'section_id',
'item_code',
'item_name',
'quantity',
'unit',
'unit_price',
'total_price',
'spec',
'note',
'created_by',
'updated_by',
'deleted_by',
];
protected $casts = [
'quantity' => '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');
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Models\ItemMaster;
use App\Traits\BelongsToTenant;
use App\Traits\ModelTrait;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class ItemField extends Model
{
use BelongsToTenant, ModelTrait, SoftDeletes;
protected $fillable = [
'tenant_id',
'section_id',
'field_name',
'field_type',
'order_no',
'is_required',
'default_value',
'placeholder',
'display_condition',
'validation_rules',
'options',
'properties',
'created_by',
'updated_by',
'deleted_by',
];
protected $casts = [
'order_no' => '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');
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Models\ItemMaster;
use App\Traits\BelongsToTenant;
use App\Traits\ModelTrait;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class ItemMasterField extends Model
{
use BelongsToTenant, ModelTrait, SoftDeletes;
protected $fillable = [
'tenant_id',
'field_name',
'field_type',
'category',
'description',
'is_common',
'default_value',
'options',
'validation_rules',
'properties',
'created_by',
'updated_by',
'deleted_by',
];
protected $casts = [
'is_common' => 'boolean',
'options' => 'array',
'validation_rules' => 'array',
'properties' => 'array',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
];
protected $hidden = [
'deleted_by',
'deleted_at',
];
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Models\ItemMaster;
use App\Traits\BelongsToTenant;
use App\Traits\ModelTrait;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class ItemPage extends Model
{
use BelongsToTenant, ModelTrait, SoftDeletes;
protected $fillable = [
'tenant_id',
'page_name',
'item_type',
'absolute_path',
'is_active',
'created_by',
'updated_by',
'deleted_by',
];
protected $casts = [
'is_active' => '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');
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Models\ItemMaster;
use App\Traits\BelongsToTenant;
use App\Traits\ModelTrait;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class ItemSection extends Model
{
use BelongsToTenant, ModelTrait, SoftDeletes;
protected $fillable = [
'tenant_id',
'page_id',
'title',
'type',
'order_no',
'created_by',
'updated_by',
'deleted_by',
];
protected $casts = [
'order_no' => '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');
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Models\ItemMaster;
use App\Traits\BelongsToTenant;
use App\Traits\ModelTrait;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class SectionTemplate extends Model
{
use BelongsToTenant, ModelTrait, SoftDeletes;
protected $fillable = [
'tenant_id',
'title',
'type',
'description',
'is_default',
'created_by',
'updated_by',
'deleted_by',
];
protected $casts = [
'is_default' => 'boolean',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
];
protected $hidden = [
'deleted_by',
'deleted_at',
];
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Models\ItemMaster;
use App\Traits\BelongsToTenant;
use App\Traits\ModelTrait;
use Illuminate\Database\Eloquent\Model;
class TabColumn extends Model
{
use BelongsToTenant, ModelTrait;
protected $fillable = [
'tenant_id',
'tab_id',
'columns',
'created_by',
'updated_by',
];
protected $casts = [
'columns' => 'array',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* 소속 탭
*/
public function tab()
{
return $this->belongsTo(CustomTab::class, 'tab_id');
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Models\ItemMaster;
use App\Traits\BelongsToTenant;
use App\Traits\ModelTrait;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class UnitOption extends Model
{
use BelongsToTenant, ModelTrait, SoftDeletes;
protected $fillable = [
'tenant_id',
'label',
'value',
'created_by',
'updated_by',
'deleted_by',
];
protected $casts = [
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
];
protected $hidden = [
'deleted_by',
'deleted_at',
];
}

View File

@@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('unit_options', function (Blueprint $table) {
$table->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');
}
};

View File

@@ -0,0 +1,44 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('section_templates', function (Blueprint $table) {
$table->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');
}
};

View File

@@ -0,0 +1,50 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('item_master_fields', function (Blueprint $table) {
$table->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');
}
};

View File

@@ -0,0 +1,45 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('item_pages', function (Blueprint $table) {
$table->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');
}
};

View File

@@ -0,0 +1,48 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('item_sections', function (Blueprint $table) {
$table->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');
}
};

View File

@@ -0,0 +1,55 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('item_fields', function (Blueprint $table) {
$table->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');
}
};

View File

@@ -0,0 +1,52 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('item_bom_items', function (Blueprint $table) {
$table->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');
}
};

View File

@@ -0,0 +1,45 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('custom_tabs', function (Blueprint $table) {
$table->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');
}
};

View File

@@ -0,0 +1,43 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('tab_columns', function (Blueprint $table) {
$table->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');
}
};

View File

@@ -0,0 +1,221 @@
<?php
namespace Database\Seeders;
use App\Models\ItemMaster\UnitOption;
use App\Models\ItemMaster\SectionTemplate;
use App\Models\ItemMaster\ItemMasterField;
use App\Models\ItemMaster\ItemPage;
use App\Models\ItemMaster\ItemSection;
use App\Models\ItemMaster\ItemField;
use App\Models\ItemMaster\ItemBomItem;
use App\Models\ItemMaster\CustomTab;
use App\Models\ItemMaster\TabColumn;
use Illuminate\Database\Seeder;
class ItemMasterSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$tenantId = 1; // 기본 테넌트 ID (실제 환경에 맞게 조정)
$userId = 1; // 기본 사용자 ID (실제 환경에 맞게 조정)
// 1. 단위 옵션
$units = [
['label' => '킬로그램', '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";
}
}