# 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 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 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 '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 'boolean', 'display_condition' => 'array', 'validation_rules' => 'array', 'options' => 'array', 'properties' => 'array', ]; /** * API 응답에서 제외할 컬럼 (내부용) */ protected $hidden = [ 'source_table', 'source_column', 'storage_type', 'json_path', ]; /** * 시스템 필드 여부 확인 */ public function isSystemField(): bool { return !is_null($this->source_table) && !is_null($this->source_column); } /** * 컬럼 직접 저장 여부 */ public function isColumnStorage(): bool { return $this->storage_type === 'column'; } /** * JSON 저장 여부 */ public function isJsonStorage(): bool { return $this->storage_type === 'json'; } } ``` --- ## 6. 시딩 데이터 ### 6.1 시더 클래스 ```php getProductFields($tenantId), $this->getMaterialFields($tenantId), $this->getBomFields($tenantId), $this->getInspectionFields($tenantId), $this->getReceiptFields($tenantId) ); foreach ($systemFields as $field) { DB::table('item_fields')->updateOrInsert( [ 'tenant_id' => $field['tenant_id'], 'source_table' => $field['source_table'], 'source_column' => $field['source_column'], ], $field ); } } private function getProductFields(int $tenantId): array { $baseFields = [ 'tenant_id' => $tenantId, 'source_table' => 'products', 'storage_type' => 'column', 'created_at' => now(), 'updated_at' => now(), ]; return [ array_merge($baseFields, [ 'source_column' => 'code', 'field_name' => '품목코드', 'field_type' => 'textbox', 'is_required' => true, 'order_no' => 1, ]), array_merge($baseFields, [ 'source_column' => 'name', 'field_name' => '품목명', 'field_type' => 'textbox', 'is_required' => true, 'order_no' => 2, ]), array_merge($baseFields, [ 'source_column' => 'unit', 'field_name' => '단위', 'field_type' => 'dropdown', 'is_required' => true, 'order_no' => 3, ]), array_merge($baseFields, [ 'source_column' => 'product_type', 'field_name' => '제품유형', 'field_type' => 'dropdown', 'order_no' => 4, 'options' => json_encode([ ['label' => '완제품', 'value' => 'FG'], ['label' => '반제품', 'value' => 'PT'], ]), ]), array_merge($baseFields, [ 'source_column' => 'category_id', 'field_name' => '카테고리', 'field_type' => 'dropdown', 'order_no' => 5, ]), array_merge($baseFields, [ 'source_column' => 'is_sellable', 'field_name' => '판매가능', 'field_type' => 'checkbox', 'order_no' => 6, 'default_value' => 'true', ]), array_merge($baseFields, [ 'source_column' => 'is_purchasable', 'field_name' => '구매가능', 'field_type' => 'checkbox', 'order_no' => 7, 'default_value' => 'false', ]), array_merge($baseFields, [ 'source_column' => 'is_producible', 'field_name' => '생산가능', 'field_type' => 'checkbox', 'order_no' => 8, 'default_value' => 'true', ]), array_merge($baseFields, [ 'source_column' => 'is_active', 'field_name' => '활성화', 'field_type' => 'checkbox', 'order_no' => 9, 'default_value' => 'true', ]), array_merge($baseFields, [ 'source_column' => 'certification_number', 'field_name' => '인증번호', 'field_type' => 'textbox', 'order_no' => 10, ]), array_merge($baseFields, [ 'source_column' => 'certification_date', 'field_name' => '인증일자', 'field_type' => 'date', 'order_no' => 11, ]), array_merge($baseFields, [ 'source_column' => 'certification_expiry', 'field_name' => '인증만료일', 'field_type' => 'date', 'order_no' => 12, ]), ]; } private function getMaterialFields(int $tenantId): array { $baseFields = [ 'tenant_id' => $tenantId, 'source_table' => 'materials', 'storage_type' => 'column', 'created_at' => now(), 'updated_at' => now(), ]; return [ array_merge($baseFields, [ 'source_column' => 'material_code', 'field_name' => '자재코드', 'field_type' => 'textbox', 'is_required' => true, 'order_no' => 1, ]), array_merge($baseFields, [ 'source_column' => 'name', 'field_name' => '자재명', 'field_type' => 'textbox', 'is_required' => true, 'order_no' => 2, ]), array_merge($baseFields, [ 'source_column' => 'item_name', 'field_name' => '품목명', 'field_type' => 'textbox', 'order_no' => 3, ]), array_merge($baseFields, [ 'source_column' => 'specification', 'field_name' => '규격', 'field_type' => 'textbox', 'order_no' => 4, ]), array_merge($baseFields, [ 'source_column' => 'unit', 'field_name' => '단위', 'field_type' => 'dropdown', 'is_required' => true, 'order_no' => 5, ]), array_merge($baseFields, [ 'source_column' => 'category_id', 'field_name' => '카테고리', 'field_type' => 'dropdown', 'order_no' => 6, ]), array_merge($baseFields, [ 'source_column' => 'is_inspection', 'field_name' => '검수필요', 'field_type' => 'checkbox', 'order_no' => 7, 'default_value' => 'false', ]), array_merge($baseFields, [ 'source_column' => 'search_tag', 'field_name' => '검색태그', 'field_type' => 'textarea', 'order_no' => 8, ]), ]; } private function getBomFields(int $tenantId): array { $baseFields = [ 'tenant_id' => $tenantId, 'source_table' => 'product_components', 'storage_type' => 'column', 'created_at' => now(), 'updated_at' => now(), ]; return [ array_merge($baseFields, [ 'source_column' => 'ref_type', 'field_name' => '참조유형', 'field_type' => 'dropdown', 'order_no' => 1, 'options' => json_encode([ ['label' => '제품', 'value' => 'product'], ['label' => '자재', 'value' => 'material'], ]), ]), array_merge($baseFields, [ 'source_column' => 'ref_id', 'field_name' => '참조품목', 'field_type' => 'dropdown', 'order_no' => 2, ]), array_merge($baseFields, [ 'source_column' => 'quantity', 'field_name' => '수량', 'field_type' => 'number', 'is_required' => true, 'order_no' => 3, 'properties' => json_encode(['precision' => 6]), ]), array_merge($baseFields, [ 'source_column' => 'formula', 'field_name' => '계산공식', 'field_type' => 'textbox', 'order_no' => 4, ]), array_merge($baseFields, [ 'source_column' => 'note', 'field_name' => '비고', 'field_type' => 'textarea', 'order_no' => 5, ]), ]; } private function getInspectionFields(int $tenantId): array { $baseFields = [ 'tenant_id' => $tenantId, 'source_table' => 'material_inspections', 'storage_type' => 'column', 'created_at' => now(), 'updated_at' => now(), ]; return [ array_merge($baseFields, [ 'source_column' => 'inspection_date', 'field_name' => '검수일', 'field_type' => 'date', 'is_required' => true, 'order_no' => 1, ]), array_merge($baseFields, [ 'source_column' => 'inspector_id', 'field_name' => '검수자', 'field_type' => 'dropdown', 'order_no' => 2, ]), array_merge($baseFields, [ 'source_column' => 'status', 'field_name' => '검수상태', 'field_type' => 'dropdown', 'order_no' => 3, 'options' => json_encode([ ['label' => '대기', 'value' => 'pending'], ['label' => '진행중', 'value' => 'in_progress'], ['label' => '완료', 'value' => 'completed'], ['label' => '불합격', 'value' => 'rejected'], ]), ]), array_merge($baseFields, [ 'source_column' => 'lot_no', 'field_name' => 'LOT번호', 'field_type' => 'textbox', 'order_no' => 4, ]), array_merge($baseFields, [ 'source_column' => 'quantity', 'field_name' => '검수수량', 'field_type' => 'number', 'order_no' => 5, ]), array_merge($baseFields, [ 'source_column' => 'passed_quantity', 'field_name' => '합격수량', 'field_type' => 'number', 'order_no' => 6, ]), array_merge($baseFields, [ 'source_column' => 'rejected_quantity', 'field_name' => '불합격수량', 'field_type' => 'number', 'order_no' => 7, ]), array_merge($baseFields, [ 'source_column' => 'note', 'field_name' => '비고', 'field_type' => 'textarea', 'order_no' => 8, ]), ]; } private function getReceiptFields(int $tenantId): array { $baseFields = [ 'tenant_id' => $tenantId, 'source_table' => 'material_receipts', 'storage_type' => 'column', 'created_at' => now(), 'updated_at' => now(), ]; return [ array_merge($baseFields, [ 'source_column' => 'receipt_date', 'field_name' => '입고일', 'field_type' => 'date', 'is_required' => true, 'order_no' => 1, ]), array_merge($baseFields, [ 'source_column' => 'lot_no', 'field_name' => 'LOT번호', 'field_type' => 'textbox', 'order_no' => 2, ]), array_merge($baseFields, [ 'source_column' => 'quantity', 'field_name' => '입고수량', 'field_type' => 'number', 'is_required' => true, 'order_no' => 3, ]), array_merge($baseFields, [ 'source_column' => 'unit_price', 'field_name' => '단가', 'field_type' => 'number', 'order_no' => 4, 'properties' => json_encode(['precision' => 4]), ]), array_merge($baseFields, [ 'source_column' => 'total_price', 'field_name' => '금액', 'field_type' => 'number', 'order_no' => 5, 'properties' => json_encode(['precision' => 4]), ]), array_merge($baseFields, [ 'source_column' => 'supplier_id', 'field_name' => '공급업체', 'field_type' => 'dropdown', 'order_no' => 6, ]), array_merge($baseFields, [ 'source_column' => 'warehouse_id', 'field_name' => '입고창고', 'field_type' => 'dropdown', 'order_no' => 7, ]), array_merge($baseFields, [ 'source_column' => 'po_number', 'field_name' => '발주번호', 'field_type' => 'textbox', 'order_no' => 8, ]), array_merge($baseFields, [ 'source_column' => 'invoice_number', 'field_name' => '송장번호', 'field_type' => 'textbox', 'order_no' => 9, ]), array_merge($baseFields, [ 'source_column' => 'note', 'field_name' => '비고', 'field_type' => 'textarea', 'order_no' => 10, ]), ]; } } ``` --- ## 7. 서비스 로직 (데이터 저장) ### 7.1 ItemDataService (신규) ```php value] 형태 * @param int|null $recordId 수정 시 레코드 ID * @return array 저장된 데이터 */ public function saveData(string $sourceTable, array $fieldValues, ?int $recordId = null): array { // 해당 테이블의 필드 매핑 정보 조회 $fields = ItemField::where('tenant_id', $this->tenantId()) ->where('source_table', $sourceTable) ->get() ->keyBy('id'); $columnData = []; // DB 컬럼 직접 저장 $jsonData = []; // JSON (attributes/options) 저장 foreach ($fieldValues as $fieldId => $value) { $field = $fields->get($fieldId); if (!$field) { // 시스템 필드가 아닌 커스텀 필드 $customField = ItemField::find($fieldId); if ($customField) { $jsonPath = $customField->json_path ?? "attributes.{$customField->field_name}"; data_set($jsonData, $jsonPath, $value); } continue; } if ($field->isColumnStorage()) { // DB 컬럼에 직접 저장 $columnData[$field->source_column] = $this->castValue($value, $field); } else { // JSON 필드에 저장 $jsonPath = $field->json_path ?? "attributes.{$field->field_name}"; data_set($jsonData, $jsonPath, $value); } } // JSON 데이터 병합 if (!empty($jsonData['attributes'])) { $columnData['attributes'] = json_encode($jsonData['attributes']); } if (!empty($jsonData['options'])) { $columnData['options'] = json_encode($jsonData['options']); } // 공통 컬럼 추가 $columnData['tenant_id'] = $this->tenantId(); $columnData['updated_by'] = $this->apiUserId(); if ($recordId) { // 수정 DB::table($sourceTable) ->where('tenant_id', $this->tenantId()) ->where('id', $recordId) ->update($columnData); return array_merge(['id' => $recordId], $columnData); } else { // 생성 $columnData['created_by'] = $this->apiUserId(); $id = DB::table($sourceTable)->insertGetId($columnData); return array_merge(['id' => $id], $columnData); } } /** * 필드 타입에 따른 값 변환 */ private function castValue($value, ItemField $field) { return match ($field->field_type) { 'number' => is_numeric($value) ? (float) $value : null, 'checkbox' => filter_var($value, FILTER_VALIDATE_BOOLEAN), 'date' => $value ? date('Y-m-d', strtotime($value)) : null, default => $value, }; } /** * 레코드 조회 시 필드 매핑 적용 */ public function getData(string $sourceTable, int $recordId): array { $record = DB::table($sourceTable) ->where('tenant_id', $this->tenantId()) ->where('id', $recordId) ->first(); if (!$record) { return []; } // 필드 매핑 정보 조회 $fields = ItemField::where('tenant_id', $this->tenantId()) ->where('source_table', $sourceTable) ->get(); $result = []; $attributes = json_decode($record->attributes ?? '{}', true); $options = json_decode($record->options ?? '{}', true); foreach ($fields as $field) { if ($field->isColumnStorage()) { $result[$field->id] = $record->{$field->source_column} ?? null; } else { $jsonPath = $field->json_path ?? "attributes.{$field->field_name}"; $result[$field->id] = data_get( ['attributes' => $attributes, 'options' => $options], $jsonPath ); } } return $result; } } ``` --- ## 8. API 영향 없음 확인 ### 8.1 기존 API 응답 (변경 없음) ```json // GET /api/v1/item-master/init { "success": true, "message": "message.fetched", "data": { "pages": [{ "id": 1, "page_name": "기본정보", "item_type": "FG", "sections": [{ "id": 1, "title": "품목코드 정보", "fields": [ { "id": 1, "field_name": "품목코드", "field_type": "textbox", "is_required": true, "order_no": 1 // source_table, source_column 등은 $hidden으로 제외됨 } ] }] }] } } ``` ### 8.2 프론트엔드 (변경 없음) - 기존 ItemMaster API 그대로 사용 - 필드 정의 조회/수정 동일 - 품목 데이터 저장 시 기존 Products/Materials API 사용 --- ## 9. 구현 순서 | 순서 | 작업 | 예상 시간 | 담당 | |------|------|----------|------| | 1 | 마이그레이션 파일 생성 및 실행 | 30분 | Backend | | 2 | ItemField 모델 수정 ($hidden 추가) | 15분 | Backend | | 3 | 시더 클래스 생성 | 1시간 | Backend | | 4 | 시딩 실행 및 데이터 확인 | 30분 | Backend | | 5 | ItemDataService 구현 | 2시간 | Backend | | 6 | 기존 ProductService/MaterialService 연동 | 2시간 | Backend | | 7 | 테스트 | 1시간 | Backend | **총 예상 시간: 7~8시간 (1일)** --- ## 10. 향후 확장 ### 10.1 신규 도메인 추가 시 1. 대상 테이블 스키마 분석 2. 시더에 필드 매핑 추가 3. 시딩 실행 4. (필요시) ItemDataService에 특수 로직 추가 ### 10.2 예정 도메인 - [ ] 회계 (accounts, journals, ledgers) - [ ] 생산 (work_orders, production_records) - [ ] 재고 (inventories, stock_movements) - [ ] 품질 (quality_controls, defect_reports) --- ## 11. 체크리스트 ### 구현 전 - [ ] 현재 item_fields 테이블 구조 확인 - [ ] 마이그레이션 롤백 계획 수립 - [ ] 기존 데이터 백업 ### 구현 중 - [ ] 마이그레이션 실행 - [ ] 모델 $hidden 적용 - [ ] 시더 실행 - [ ] API 응답 검증 (매핑 컬럼 미노출 확인) ### 구현 후 - [ ] 기존 ItemMaster API 정상 동작 확인 - [ ] 프론트엔드 영향 없음 확인 - [ ] 품목 저장 시 매핑 정상 동작 확인 --- **문서 끝**