662 lines
27 KiB
Markdown
662 lines
27 KiB
Markdown
|
|
# ItemMaster 연동 설계서
|
||
|
|
|
||
|
|
**작성일**: 2025-12-05
|
||
|
|
**최종 수정**: 2025-12-08
|
||
|
|
**버전**: 1.1
|
||
|
|
**상태**: Draft
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 1. 개요
|
||
|
|
|
||
|
|
### 1.1 목적
|
||
|
|
품목기준관리(ItemMaster)에서 정의한 필드와 실제 엔티티 데이터를 연동하여, 동적 필드 정의 및 값 저장을 가능하게 한다.
|
||
|
|
|
||
|
|
### 1.2 설계 원칙
|
||
|
|
- **기존 테이블 활용**: 신규 테이블 추가 없이 기존 `attributes` JSON 컬럼 활용
|
||
|
|
- **범용성**: 품목(products, materials) 외에도 다른 엔티티(orders, clients 등) 확장 가능
|
||
|
|
- **성능**: JOIN 없이 단일 쿼리로 조회 가능
|
||
|
|
- **유연성**: 테넌트/그룹별 다른 필드 구성 지원
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 2. 현재 구조
|
||
|
|
|
||
|
|
### 2.1 ItemMaster 테이블 구조
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────────────────────────────────┐
|
||
|
|
│ item_pages (페이지 정의) │
|
||
|
|
├─────────────────────────────────────────────────────────────┤
|
||
|
|
│ id, tenant_id, page_name, item_type, source_table, │
|
||
|
|
│ is_active │
|
||
|
|
│ │
|
||
|
|
│ item_type: FG(완제품), PT(반제품), SM(부자재), │
|
||
|
|
│ RM(원자재), CS(소모품) │
|
||
|
|
│ │
|
||
|
|
│ source_table: 실제 저장 테이블명 │
|
||
|
|
│ - 'products' (FG, PT) │
|
||
|
|
│ - 'materials' (SM, RM, CS) │
|
||
|
|
└─────────────────────────────────────────────────────────────┘
|
||
|
|
│
|
||
|
|
│ 1:N
|
||
|
|
▼
|
||
|
|
┌─────────────────────────────────────────────────────────────┐
|
||
|
|
│ item_sections (섹션 정의) │
|
||
|
|
├─────────────────────────────────────────────────────────────┤
|
||
|
|
│ id, tenant_id, page_id, title, type, order_no │
|
||
|
|
│ │
|
||
|
|
│ type: fields(필드형), bom(BOM형) │
|
||
|
|
└─────────────────────────────────────────────────────────────┘
|
||
|
|
│
|
||
|
|
│ 1:N
|
||
|
|
▼
|
||
|
|
┌─────────────────────────────────────────────────────────────┐
|
||
|
|
│ item_fields (필드 정의) │
|
||
|
|
├─────────────────────────────────────────────────────────────┤
|
||
|
|
│ id, tenant_id, group_id, section_id (nullable) │
|
||
|
|
│ field_key ← attributes JSON 키와 매핑 │
|
||
|
|
│ field_name ← 화면 표시명 │
|
||
|
|
│ field_type ← textbox, number, dropdown, checkbox... │
|
||
|
|
│ is_required ← 필수 여부 │
|
||
|
|
│ default_value ← 기본값 │
|
||
|
|
│ placeholder ← 입력 힌트 │
|
||
|
|
│ validation_rules ← 검증 규칙 JSON │
|
||
|
|
│ options ← 선택 옵션 JSON │
|
||
|
|
│ properties ← 추가 속성 JSON │
|
||
|
|
│ category ← 필드 카테고리 │
|
||
|
|
│ is_common ← 공통 필드 여부 │
|
||
|
|
│ is_active ← 활성 여부 │
|
||
|
|
│ │
|
||
|
|
│ [내부용 매핑 컬럼 - API 응답에서 hidden] │
|
||
|
|
│ source_table ← 원본 테이블명 (products, materials 등) │
|
||
|
|
│ source_column ← 원본 컬럼명 (code, name 등) │
|
||
|
|
│ storage_type ← 저장방식 (column=DB컬럼, json=JSON) │
|
||
|
|
│ json_path ← JSON 저장 경로 (예: attributes.size) │
|
||
|
|
└─────────────────────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2.2 엔티티 테이블 구조
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────────────────────────────────┐
|
||
|
|
│ products │
|
||
|
|
├─────────────────────────────────────────────────────────────┤
|
||
|
|
│ [고정 필드] │
|
||
|
|
│ id, tenant_id, code, name, unit, category_id │
|
||
|
|
│ product_type, is_active, is_sellable, is_purchasable... │
|
||
|
|
│ │
|
||
|
|
│ [동적 필드] │
|
||
|
|
│ attributes JSON ← ItemMaster 필드 값 저장 │
|
||
|
|
└─────────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
┌─────────────────────────────────────────────────────────────┐
|
||
|
|
│ materials │
|
||
|
|
├─────────────────────────────────────────────────────────────┤
|
||
|
|
│ [고정 필드] │
|
||
|
|
│ id, tenant_id, material_code, name, unit, category_id │
|
||
|
|
│ material_type, is_active... │
|
||
|
|
│ │
|
||
|
|
│ [동적 필드] │
|
||
|
|
│ attributes JSON ← ItemMaster 필드 값 저장 │
|
||
|
|
│ options JSON ← 추가 옵션 저장 │
|
||
|
|
└─────────────────────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 3. 연동 설계
|
||
|
|
|
||
|
|
### 3.1 매핑 규칙
|
||
|
|
|
||
|
|
```
|
||
|
|
ItemMaster Entity.attributes
|
||
|
|
┌──────────────────────┐ ┌──────────────────────┐
|
||
|
|
│ group_id: 1 │ │ │
|
||
|
|
│ field_key: "color" │ ◀═══매핑═══▶ │ {"color": "빨강"} │
|
||
|
|
│ field_key: "weight" │ ◀═══매핑═══▶ │ {"weight": 1.5} │
|
||
|
|
│ field_key: "spec" │ ◀═══매핑═══▶ │ {"spec": "10x20"} │
|
||
|
|
└──────────────────────┘ └──────────────────────┘
|
||
|
|
|
||
|
|
핵심: item_fields.field_key = attributes JSON의 key
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.2 Group ID 정의
|
||
|
|
|
||
|
|
| group_id | 엔티티 | 대상 테이블 | 비고 |
|
||
|
|
|----------|--------|-------------|------|
|
||
|
|
| 1 | 품목-제품 | products | product_type: FG, PT |
|
||
|
|
| 2 | 품목-자재 | materials | material_type: SM, RM, CS |
|
||
|
|
| 3 | 주문 | orders | 향후 확장 |
|
||
|
|
| 4 | 고객 | clients | 향후 확장 |
|
||
|
|
| ... | ... | ... | 필요 시 추가 |
|
||
|
|
|
||
|
|
> **참고**: group_id는 `common_codes` 테이블에서 관리하거나, 별도 enum으로 정의 가능
|
||
|
|
|
||
|
|
### 3.3 데이터 흐름
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────────────────────────────────┐
|
||
|
|
│ 1. 관리자: ItemMaster에서 필드 정의 │
|
||
|
|
├─────────────────────────────────────────────────────────────┤
|
||
|
|
│ │
|
||
|
|
│ POST /api/v1/item-master/fields │
|
||
|
|
│ { │
|
||
|
|
│ "group_id": 1, │
|
||
|
|
│ "field_key": "color", │
|
||
|
|
│ "field_name": "색상", │
|
||
|
|
│ "field_type": "dropdown", │
|
||
|
|
│ "is_required": true, │
|
||
|
|
│ "options": [ │
|
||
|
|
│ {"label": "빨강", "value": "red"}, │
|
||
|
|
│ {"label": "파랑", "value": "blue"} │
|
||
|
|
│ ] │
|
||
|
|
│ } │
|
||
|
|
│ │
|
||
|
|
│ → item_fields 테이블에 저장 │
|
||
|
|
└─────────────────────────────────────────────────────────────┘
|
||
|
|
│
|
||
|
|
▼
|
||
|
|
┌─────────────────────────────────────────────────────────────┐
|
||
|
|
│ 2. 사용자: 품목 등록 화면 진입 │
|
||
|
|
├─────────────────────────────────────────────────────────────┤
|
||
|
|
│ │
|
||
|
|
│ GET /api/v1/item-master/fields?group_id=1 │
|
||
|
|
│ │
|
||
|
|
│ → 정의된 필드 목록 반환 │
|
||
|
|
│ → 프론트엔드가 동적 폼 렌더링 │
|
||
|
|
│ │
|
||
|
|
│ ┌────────────────────────────────────┐ │
|
||
|
|
│ │ [색상 ▼] ← dropdown으로 표시 │ │
|
||
|
|
│ │ 빨강 │ │
|
||
|
|
│ │ 파랑 │ │
|
||
|
|
│ └────────────────────────────────────┘ │
|
||
|
|
└─────────────────────────────────────────────────────────────┘
|
||
|
|
│
|
||
|
|
▼
|
||
|
|
┌─────────────────────────────────────────────────────────────┐
|
||
|
|
│ 3. 사용자: 품목 저장 │
|
||
|
|
├─────────────────────────────────────────────────────────────┤
|
||
|
|
│ │
|
||
|
|
│ POST /api/v1/products │
|
||
|
|
│ { │
|
||
|
|
│ "code": "P-001", ← 고정 필드 │
|
||
|
|
│ "name": "티셔츠", │
|
||
|
|
│ "unit": "EA", │
|
||
|
|
│ "product_type": "FG", │
|
||
|
|
│ "attributes": { ← 동적 필드 │
|
||
|
|
│ "color": "red", (field_key: value) │
|
||
|
|
│ "size": "XL" │
|
||
|
|
│ } │
|
||
|
|
│ } │
|
||
|
|
│ │
|
||
|
|
│ → products 테이블에 저장 │
|
||
|
|
│ → attributes JSON에 동적 필드 값 포함 │
|
||
|
|
└─────────────────────────────────────────────────────────────┘
|
||
|
|
│
|
||
|
|
▼
|
||
|
|
┌─────────────────────────────────────────────────────────────┐
|
||
|
|
│ 4. 사용자: 품목 조회 │
|
||
|
|
├─────────────────────────────────────────────────────────────┤
|
||
|
|
│ │
|
||
|
|
│ GET /api/v1/products/1 │
|
||
|
|
│ │
|
||
|
|
│ { │
|
||
|
|
│ "id": 1, │
|
||
|
|
│ "code": "P-001", │
|
||
|
|
│ "name": "티셔츠", │
|
||
|
|
│ "attributes": { │
|
||
|
|
│ "color": "red", │
|
||
|
|
│ "size": "XL" │
|
||
|
|
│ } │
|
||
|
|
│ } │
|
||
|
|
│ │
|
||
|
|
│ → JOIN 없이 한 번에 조회! │
|
||
|
|
└─────────────────────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 4. API 설계
|
||
|
|
|
||
|
|
### 4.1 ItemMaster API (기존)
|
||
|
|
|
||
|
|
| Method | Endpoint | 설명 |
|
||
|
|
|--------|----------|------|
|
||
|
|
| GET | `/api/v1/item-master/fields` | 필드 목록 조회 |
|
||
|
|
| GET | `/api/v1/item-master/fields/{id}` | 필드 상세 조회 |
|
||
|
|
| POST | `/api/v1/item-master/fields` | 필드 생성 |
|
||
|
|
| PUT | `/api/v1/item-master/fields/{id}` | 필드 수정 |
|
||
|
|
| DELETE | `/api/v1/item-master/fields/{id}` | 필드 삭제 |
|
||
|
|
|
||
|
|
**필터 파라미터**:
|
||
|
|
- `group_id`: 엔티티 그룹 필터
|
||
|
|
- `section_id`: 섹션 필터
|
||
|
|
- `is_active`: 활성 필터
|
||
|
|
- `is_common`: 공통 필드 필터
|
||
|
|
|
||
|
|
### 4.2 엔티티 API 수정
|
||
|
|
|
||
|
|
#### 4.2.1 Products API
|
||
|
|
|
||
|
|
**저장 시 attributes 포함**:
|
||
|
|
```json
|
||
|
|
POST /api/v1/products
|
||
|
|
{
|
||
|
|
"code": "P-001",
|
||
|
|
"name": "제품명",
|
||
|
|
"unit": "EA",
|
||
|
|
"product_type": "FG",
|
||
|
|
"attributes": {
|
||
|
|
"color": "red",
|
||
|
|
"weight": 1.5,
|
||
|
|
"custom_field": "value"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**조회 시 필드 메타데이터 포함 (선택)**:
|
||
|
|
```
|
||
|
|
GET /api/v1/products/1?include_field_meta=true
|
||
|
|
```
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"id": 1,
|
||
|
|
"code": "P-001",
|
||
|
|
"name": "제품명",
|
||
|
|
"attributes": {
|
||
|
|
"color": "red",
|
||
|
|
"weight": 1.5
|
||
|
|
},
|
||
|
|
"field_meta": [
|
||
|
|
{
|
||
|
|
"field_key": "color",
|
||
|
|
"field_name": "색상",
|
||
|
|
"field_type": "dropdown",
|
||
|
|
"value": "red",
|
||
|
|
"options": [...]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"field_key": "weight",
|
||
|
|
"field_name": "중량",
|
||
|
|
"field_type": "number",
|
||
|
|
"value": 1.5
|
||
|
|
}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 5. 검증 로직
|
||
|
|
|
||
|
|
### 5.1 저장 시 검증 흐름
|
||
|
|
|
||
|
|
```php
|
||
|
|
class ItemFieldValidationService
|
||
|
|
{
|
||
|
|
/**
|
||
|
|
* attributes 값을 ItemMaster 기준으로 검증
|
||
|
|
*/
|
||
|
|
public function validate(int $groupId, array $attributes): array
|
||
|
|
{
|
||
|
|
$errors = [];
|
||
|
|
|
||
|
|
// 1. 해당 그룹의 필드 정의 조회
|
||
|
|
$fields = ItemField::where('group_id', $groupId)
|
||
|
|
->where('is_active', true)
|
||
|
|
->get()
|
||
|
|
->keyBy('field_key');
|
||
|
|
|
||
|
|
// 2. 필수 필드 체크
|
||
|
|
foreach ($fields->where('is_required', true) as $field) {
|
||
|
|
if (!isset($attributes[$field->field_key])) {
|
||
|
|
$errors[$field->field_key] = "{$field->field_name}은(는) 필수입니다.";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. 타입별 검증
|
||
|
|
foreach ($attributes as $key => $value) {
|
||
|
|
if (!$fields->has($key)) {
|
||
|
|
continue; // 정의되지 않은 필드는 스킵 (또는 에러)
|
||
|
|
}
|
||
|
|
|
||
|
|
$field = $fields->get($key);
|
||
|
|
$fieldError = $this->validateFieldValue($field, $value);
|
||
|
|
if ($fieldError) {
|
||
|
|
$errors[$key] = $fieldError;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $errors;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 필드 타입별 값 검증
|
||
|
|
*/
|
||
|
|
private function validateFieldValue(ItemField $field, mixed $value): ?string
|
||
|
|
{
|
||
|
|
return match($field->field_type) {
|
||
|
|
'number' => $this->validateNumber($field, $value),
|
||
|
|
'dropdown' => $this->validateDropdown($field, $value),
|
||
|
|
'date' => $this->validateDate($field, $value),
|
||
|
|
'checkbox' => $this->validateCheckbox($field, $value),
|
||
|
|
default => null
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
private function validateNumber(ItemField $field, mixed $value): ?string
|
||
|
|
{
|
||
|
|
if (!is_numeric($value)) {
|
||
|
|
return "{$field->field_name}은(는) 숫자여야 합니다.";
|
||
|
|
}
|
||
|
|
|
||
|
|
$rules = $field->validation_rules ?? [];
|
||
|
|
if (isset($rules['min']) && $value < $rules['min']) {
|
||
|
|
return "{$field->field_name}은(는) {$rules['min']} 이상이어야 합니다.";
|
||
|
|
}
|
||
|
|
if (isset($rules['max']) && $value > $rules['max']) {
|
||
|
|
return "{$field->field_name}은(는) {$rules['max']} 이하여야 합니다.";
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
private function validateDropdown(ItemField $field, mixed $value): ?string
|
||
|
|
{
|
||
|
|
$options = $field->options ?? [];
|
||
|
|
$validValues = array_column($options, 'value');
|
||
|
|
|
||
|
|
if (!in_array($value, $validValues)) {
|
||
|
|
return "{$field->field_name}의 값이 유효하지 않습니다.";
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 5.2 Controller에서 사용
|
||
|
|
|
||
|
|
```php
|
||
|
|
class ProductsController extends Controller
|
||
|
|
{
|
||
|
|
public function store(ProductStoreRequest $request)
|
||
|
|
{
|
||
|
|
$validated = $request->validated();
|
||
|
|
|
||
|
|
// attributes 검증 (선택적)
|
||
|
|
if (isset($validated['attributes'])) {
|
||
|
|
$groupId = 1; // 품목-제품 그룹
|
||
|
|
$errors = $this->fieldValidationService->validate(
|
||
|
|
$groupId,
|
||
|
|
$validated['attributes']
|
||
|
|
);
|
||
|
|
|
||
|
|
if (!empty($errors)) {
|
||
|
|
return ApiResponse::error('검증 실패', $errors, 422);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$product = $this->productService->create($validated);
|
||
|
|
|
||
|
|
return ApiResponse::success($product, __('message.created'));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 6. 프론트엔드 연동
|
||
|
|
|
||
|
|
### 6.1 동적 폼 렌더링 흐름
|
||
|
|
|
||
|
|
```
|
||
|
|
1. 페이지 로드 시
|
||
|
|
GET /api/v1/item-master/fields?group_id=1
|
||
|
|
|
||
|
|
2. 필드 정의 기반 폼 컴포넌트 렌더링
|
||
|
|
field_type: textbox → <Input />
|
||
|
|
field_type: number → <InputNumber />
|
||
|
|
field_type: dropdown → <Select options={field.options} />
|
||
|
|
field_type: checkbox → <Checkbox />
|
||
|
|
field_type: date → <DatePicker />
|
||
|
|
field_type: textarea → <Textarea />
|
||
|
|
|
||
|
|
3. 저장 시 attributes 객체 구성
|
||
|
|
{
|
||
|
|
[field_key]: value,
|
||
|
|
[field_key]: value,
|
||
|
|
...
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 6.2 React 컴포넌트 예시
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
interface ItemField {
|
||
|
|
id: number;
|
||
|
|
field_key: string;
|
||
|
|
field_name: string;
|
||
|
|
field_type: 'textbox' | 'number' | 'dropdown' | 'checkbox' | 'date' | 'textarea';
|
||
|
|
is_required: boolean;
|
||
|
|
default_value?: string;
|
||
|
|
placeholder?: string;
|
||
|
|
options?: Array<{ label: string; value: string }>;
|
||
|
|
validation_rules?: Record<string, any>;
|
||
|
|
}
|
||
|
|
|
||
|
|
function DynamicFieldRenderer({ field, value, onChange }: Props) {
|
||
|
|
switch (field.field_type) {
|
||
|
|
case 'textbox':
|
||
|
|
return (
|
||
|
|
<Input
|
||
|
|
value={value}
|
||
|
|
onChange={(e) => onChange(field.field_key, e.target.value)}
|
||
|
|
placeholder={field.placeholder}
|
||
|
|
required={field.is_required}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
|
||
|
|
case 'number':
|
||
|
|
return (
|
||
|
|
<InputNumber
|
||
|
|
value={value}
|
||
|
|
onChange={(val) => onChange(field.field_key, val)}
|
||
|
|
min={field.validation_rules?.min}
|
||
|
|
max={field.validation_rules?.max}
|
||
|
|
required={field.is_required}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
|
||
|
|
case 'dropdown':
|
||
|
|
return (
|
||
|
|
<Select
|
||
|
|
value={value}
|
||
|
|
onChange={(val) => onChange(field.field_key, val)}
|
||
|
|
options={field.options}
|
||
|
|
required={field.is_required}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
|
||
|
|
// ... 기타 타입
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function ProductForm() {
|
||
|
|
const [fields, setFields] = useState<ItemField[]>([]);
|
||
|
|
const [attributes, setAttributes] = useState<Record<string, any>>({});
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
// 필드 정의 로드
|
||
|
|
fetch('/api/v1/item-master/fields?group_id=1')
|
||
|
|
.then(res => res.json())
|
||
|
|
.then(data => setFields(data.data));
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const handleFieldChange = (key: string, value: any) => {
|
||
|
|
setAttributes(prev => ({ ...prev, [key]: value }));
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<form>
|
||
|
|
{/* 고정 필드 */}
|
||
|
|
<Input name="code" label="품목코드" required />
|
||
|
|
<Input name="name" label="품목명" required />
|
||
|
|
|
||
|
|
{/* 동적 필드 */}
|
||
|
|
{fields.map(field => (
|
||
|
|
<DynamicFieldRenderer
|
||
|
|
key={field.id}
|
||
|
|
field={field}
|
||
|
|
value={attributes[field.field_key]}
|
||
|
|
onChange={handleFieldChange}
|
||
|
|
/>
|
||
|
|
))}
|
||
|
|
</form>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 7. 확장 가이드
|
||
|
|
|
||
|
|
### 7.1 새 엔티티 추가 시
|
||
|
|
|
||
|
|
1. **group_id 정의**: 새 그룹 ID 할당
|
||
|
|
2. **테이블 확인**: `attributes` JSON 컬럼 존재 확인 (없으면 추가)
|
||
|
|
3. **ItemMaster 필드 정의**: 해당 group_id로 필드 생성
|
||
|
|
4. **API 수정**: 저장/조회 시 attributes 처리 로직 추가
|
||
|
|
|
||
|
|
### 7.2 예시: 주문(orders) 연동
|
||
|
|
|
||
|
|
```sql
|
||
|
|
-- 1. orders 테이블에 attributes 컬럼 추가 (없는 경우)
|
||
|
|
ALTER TABLE orders ADD COLUMN attributes JSON DEFAULT NULL COMMENT '동적 필드';
|
||
|
|
|
||
|
|
-- 2. ItemMaster에 주문용 필드 정의
|
||
|
|
INSERT INTO item_fields (tenant_id, group_id, field_key, field_name, field_type, ...)
|
||
|
|
VALUES (1, 3, 'urgency', '긴급도', 'dropdown', ...);
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 8. 모델 헬퍼 메서드
|
||
|
|
|
||
|
|
### 8.1 ItemPage 모델
|
||
|
|
|
||
|
|
```php
|
||
|
|
class ItemPage extends Model
|
||
|
|
{
|
||
|
|
/**
|
||
|
|
* source_table에 해당하는 모델 클래스명 반환
|
||
|
|
*/
|
||
|
|
public function getTargetModelClass(): ?string
|
||
|
|
{
|
||
|
|
$mapping = [
|
||
|
|
'products' => \App\Models\Product::class,
|
||
|
|
'materials' => \App\Models\Material::class,
|
||
|
|
];
|
||
|
|
return $mapping[$this->source_table] ?? null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 제품 페이지인지 확인
|
||
|
|
*/
|
||
|
|
public function isProductPage(): bool
|
||
|
|
{
|
||
|
|
return $this->source_table === 'products';
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 자재 페이지인지 확인
|
||
|
|
*/
|
||
|
|
public function isMaterialPage(): bool
|
||
|
|
{
|
||
|
|
return $this->source_table === 'materials';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 8.2 ItemField 모델
|
||
|
|
|
||
|
|
```php
|
||
|
|
class ItemField extends Model
|
||
|
|
{
|
||
|
|
/**
|
||
|
|
* 시스템 필드 여부 확인 (DB 컬럼과 매핑된 필드)
|
||
|
|
*/
|
||
|
|
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';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 8.3 필드 저장 방식 판단
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────────────────────────────────┐
|
||
|
|
│ storage_type 판단 로직 │
|
||
|
|
├─────────────────────────────────────────────────────────────┤
|
||
|
|
│ │
|
||
|
|
│ if (source_table && source_column) { │
|
||
|
|
│ // 시스템 필드 (기존 DB 컬럼과 매핑) │
|
||
|
|
│ if (storage_type === 'column') { │
|
||
|
|
│ → products.{source_column} 또는 │
|
||
|
|
│ materials.{source_column} 에서 직접 읽기/쓰기 │
|
||
|
|
│ } else if (storage_type === 'json') { │
|
||
|
|
│ → {json_path} 경로로 JSON 내 읽기/쓰기 │
|
||
|
|
│ } │
|
||
|
|
│ } else { │
|
||
|
|
│ // 커스텀 필드 (동적 정의) │
|
||
|
|
│ → attributes.{field_key} 에 저장 │
|
||
|
|
│ } │
|
||
|
|
│ │
|
||
|
|
└─────────────────────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 9. 구현 계획
|
||
|
|
|
||
|
|
| 순서 | 작업 | 담당 | 예상 공수 |
|
||
|
|
|------|------|------|----------|
|
||
|
|
| 1 | group_id 코드 정의 | BE | 0.5일 |
|
||
|
|
| 2 | ItemFieldValidationService 구현 | BE | 1일 |
|
||
|
|
| 3 | ProductsController 수정 (검증 연동) | BE | 0.5일 |
|
||
|
|
| 4 | MaterialsController 수정 (검증 연동) | BE | 0.5일 |
|
||
|
|
| 5 | API 응답에 field_meta 포함 옵션 | BE | 0.5일 |
|
||
|
|
| 6 | DynamicFieldRenderer 컴포넌트 | FE | 2일 |
|
||
|
|
| 7 | 품목 등록/수정 폼 연동 | FE | 1일 |
|
||
|
|
| 8 | 테스트 및 QA | 공통 | 1일 |
|
||
|
|
|
||
|
|
**총 예상 공수: 7일**
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 10. 변경 이력
|
||
|
|
|
||
|
|
| 날짜 | 버전 | 변경 내용 | 작성자 |
|
||
|
|
|------|------|----------|--------|
|
||
|
|
| 2025-12-05 | 1.0 | 최초 작성 | - |
|
||
|
|
| 2025-12-08 | 1.1 | source_table/source_column 매핑 컬럼 추가, 모델 헬퍼 메서드 문서화 | - |
|