1166 lines
40 KiB
Markdown
1166 lines
40 KiB
Markdown
|
|
# ItemMaster 범용 메타 필드 시스템 구현 계획
|
||
|
|
|
||
|
|
**작성일**: 2025-12-08
|
||
|
|
**버전**: v1.2
|
||
|
|
**상태**: Draft (검토 필요)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 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 현재 구조 (item_type 기반)
|
||
|
|
|
||
|
|
```
|
||
|
|
item_master_pages.item_type
|
||
|
|
┌─────────────────────────────────────────┐
|
||
|
|
│ FG (완제품) ──┐ │
|
||
|
|
│ PT (반제품) ──┴──→ products 테이블 │
|
||
|
|
│ │
|
||
|
|
│ SM (부자재) ──┐ │
|
||
|
|
│ RM (원자재) ──┼──→ materials 테이블 │
|
||
|
|
│ CS (소모품) ──┘ │
|
||
|
|
└─────────────────────────────────────────┘
|
||
|
|
|
||
|
|
문제점:
|
||
|
|
- 회계, 생산 등 새 도메인 추가 시 item_type 의미가 맞지 않음
|
||
|
|
- 테이블 분기 로직이 코드에 하드코딩됨
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2.2 변경 구조 (단순화)
|
||
|
|
|
||
|
|
#### 2.2.1 common_codes에 item_type 그룹 추가
|
||
|
|
|
||
|
|
```
|
||
|
|
common_codes (code_group = 'item_type')
|
||
|
|
┌────────────┬────────┬──────────┐
|
||
|
|
│ code_group │ code │ name │
|
||
|
|
├────────────┼────────┼──────────┤
|
||
|
|
│ item_type │ FG │ 완제품 │
|
||
|
|
│ item_type │ PT │ 반제품 │
|
||
|
|
│ item_type │ SM │ 부자재 │
|
||
|
|
│ item_type │ RM │ 원자재 │
|
||
|
|
│ item_type │ CS │ 소모품 │
|
||
|
|
└────────────┴────────┴──────────┘
|
||
|
|
|
||
|
|
→ code_group = 'item_type' (컬럼명과 동일 = 직관적!)
|
||
|
|
→ 계층 구조 없음 (단순)
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2.2.2 item_master_pages 테이블 변경
|
||
|
|
|
||
|
|
```
|
||
|
|
item_master_pages (변경 후)
|
||
|
|
┌────┬──────────┬────────────┬──────────────────┐
|
||
|
|
│ id │ group_id │ item_type │ source_table │
|
||
|
|
├────┼──────────┼────────────┼──────────────────┤
|
||
|
|
│ 1 │ 1 │ FG │ products │
|
||
|
|
│ 2 │ 1 │ PT │ products │
|
||
|
|
│ 3 │ 1 │ SM │ materials │ ← 모두 group_id=1 (품목관리)
|
||
|
|
│ 4 │ 1 │ RM │ materials │
|
||
|
|
│ 5 │ 1 │ CS │ materials │
|
||
|
|
├────┼──────────┼────────────┼──────────────────┤
|
||
|
|
│ 6 │ 2 │ JOURNAL │ journals │ ← group_id=2 (회계) - 향후 확장
|
||
|
|
│ 7 │ 3 │ WO │ work_orders │ ← group_id=3 (생산) - 향후 확장
|
||
|
|
└────┴──────────┴────────────┴──────────────────┘
|
||
|
|
|
||
|
|
→ group_id: 테이블 내 자체 그룹핑 (1=품목관리, 2=회계, 3=생산)
|
||
|
|
→ item_type: 키! common_codes와 매핑
|
||
|
|
→ source_table: 실제 저장할 테이블명 (새 컬럼!)
|
||
|
|
→ page_name: 삭제 (common_codes.name으로 JOIN 조회)
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2.2.3 매핑 조회
|
||
|
|
|
||
|
|
```sql
|
||
|
|
-- item_type 컬럼명 = code_group 이름 → 직관적!
|
||
|
|
SELECT
|
||
|
|
p.*,
|
||
|
|
c.name as page_name
|
||
|
|
FROM item_master_pages p
|
||
|
|
JOIN common_codes c
|
||
|
|
ON c.code_group = 'item_type' -- 컬럼명과 동일!
|
||
|
|
AND c.code = p.item_type
|
||
|
|
WHERE p.group_id = 1; -- 품목관리 그룹
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 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
|
||
|
|
<?php
|
||
|
|
|
||
|
|
use Illuminate\Database\Migrations\Migration;
|
||
|
|
use Illuminate\Database\Schema\Blueprint;
|
||
|
|
use Illuminate\Support\Facades\Schema;
|
||
|
|
|
||
|
|
return new class extends Migration
|
||
|
|
{
|
||
|
|
public function up(): void
|
||
|
|
{
|
||
|
|
Schema::table('item_fields', function (Blueprint $table) {
|
||
|
|
// 내부용 매핑 컬럼 (API 응답에서 제외)
|
||
|
|
$table->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 변경
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
|
||
|
|
use Illuminate\Database\Migrations\Migration;
|
||
|
|
use Illuminate\Database\Schema\Blueprint;
|
||
|
|
use Illuminate\Support\Facades\Schema;
|
||
|
|
|
||
|
|
return new class extends Migration
|
||
|
|
{
|
||
|
|
public function up(): void
|
||
|
|
{
|
||
|
|
Schema::table('item_pages', function (Blueprint $table) {
|
||
|
|
// source_table 컬럼 추가
|
||
|
|
$table->string('source_table', 100)
|
||
|
|
->nullable()
|
||
|
|
->after('item_type')
|
||
|
|
->comment('실제 저장 테이블명 (products, materials 등)');
|
||
|
|
|
||
|
|
// page_name 삭제 (common_codes.name으로 대체)
|
||
|
|
$table->dropColumn('page_name');
|
||
|
|
|
||
|
|
// 인덱스
|
||
|
|
$table->index('source_table', 'idx_source_table');
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
public function down(): void
|
||
|
|
{
|
||
|
|
Schema::table('item_pages', function (Blueprint $table) {
|
||
|
|
$table->dropIndex('idx_source_table');
|
||
|
|
$table->dropColumn('source_table');
|
||
|
|
$table->string('page_name', 100)->after('item_type');
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4.4 common_codes 시더 (item_type)
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
|
||
|
|
namespace Database\Seeders;
|
||
|
|
|
||
|
|
use Illuminate\Database\Seeder;
|
||
|
|
use Illuminate\Support\Facades\DB;
|
||
|
|
|
||
|
|
class ItemTypeSeeder extends Seeder
|
||
|
|
{
|
||
|
|
public function run(): void
|
||
|
|
{
|
||
|
|
$tenantId = 1; // 기본 테넌트
|
||
|
|
|
||
|
|
// code_group = 'item_type' (컬럼명과 동일하게!)
|
||
|
|
$itemTypes = [
|
||
|
|
['code_group' => 'item_type', 'code' => 'FG', 'name' => '완제품', 'tenant_id' => $tenantId],
|
||
|
|
['code_group' => 'item_type', 'code' => 'PT', 'name' => '반제품', 'tenant_id' => $tenantId],
|
||
|
|
['code_group' => 'item_type', 'code' => 'SM', 'name' => '부자재', 'tenant_id' => $tenantId],
|
||
|
|
['code_group' => 'item_type', 'code' => 'RM', 'name' => '원자재', 'tenant_id' => $tenantId],
|
||
|
|
['code_group' => 'item_type', 'code' => 'CS', 'name' => '소모품', 'tenant_id' => $tenantId],
|
||
|
|
];
|
||
|
|
|
||
|
|
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(),
|
||
|
|
])
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 5. 모델 수정
|
||
|
|
|
||
|
|
### 5.1 ItemField 모델
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
|
||
|
|
namespace App\Models\ItemMaster;
|
||
|
|
|
||
|
|
use Illuminate\Database\Eloquent\Model;
|
||
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||
|
|
use App\Traits\BelongsToTenant;
|
||
|
|
|
||
|
|
class ItemField extends Model
|
||
|
|
{
|
||
|
|
use BelongsToTenant, SoftDeletes;
|
||
|
|
|
||
|
|
protected $table = 'item_fields';
|
||
|
|
|
||
|
|
protected $fillable = [
|
||
|
|
'tenant_id',
|
||
|
|
'section_id',
|
||
|
|
'group_id',
|
||
|
|
'field_name',
|
||
|
|
'field_type',
|
||
|
|
'order_no',
|
||
|
|
'is_required',
|
||
|
|
'default_value',
|
||
|
|
'placeholder',
|
||
|
|
'display_condition',
|
||
|
|
'validation_rules',
|
||
|
|
'options',
|
||
|
|
'properties',
|
||
|
|
// 내부용 매핑 컬럼
|
||
|
|
'source_table',
|
||
|
|
'source_column',
|
||
|
|
'storage_type',
|
||
|
|
'json_path',
|
||
|
|
];
|
||
|
|
|
||
|
|
protected $casts = [
|
||
|
|
'is_required' => '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
|
||
|
|
<?php
|
||
|
|
|
||
|
|
namespace Database\Seeders;
|
||
|
|
|
||
|
|
use Illuminate\Database\Seeder;
|
||
|
|
use Illuminate\Support\Facades\DB;
|
||
|
|
|
||
|
|
class ItemSystemFieldsSeeder extends Seeder
|
||
|
|
{
|
||
|
|
public function run(): void
|
||
|
|
{
|
||
|
|
$tenantId = 1; // 기본 테넌트 (또는 동적으로 처리)
|
||
|
|
|
||
|
|
$systemFields = array_merge(
|
||
|
|
$this->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
|
||
|
|
<?php
|
||
|
|
|
||
|
|
namespace App\Services\ItemMaster;
|
||
|
|
|
||
|
|
use App\Models\ItemMaster\ItemField;
|
||
|
|
use App\Services\Service;
|
||
|
|
use Illuminate\Support\Facades\DB;
|
||
|
|
|
||
|
|
class ItemDataService extends Service
|
||
|
|
{
|
||
|
|
/**
|
||
|
|
* 필드 값을 적절한 테이블/컬럼에 저장
|
||
|
|
*
|
||
|
|
* @param string $sourceTable 대상 테이블 (products, materials 등)
|
||
|
|
* @param array $fieldValues [field_id => 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 정상 동작 확인
|
||
|
|
- [ ] 프론트엔드 영향 없음 확인
|
||
|
|
- [ ] 품목 저장 시 매핑 정상 동작 확인
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
**문서 끝**
|