diff --git a/changes/20260128_kd_items_migration_phase1.md b/changes/20260128_kd_items_migration_phase1.md new file mode 100644 index 0000000..a5db013 --- /dev/null +++ b/changes/20260128_kd_items_migration_phase1.md @@ -0,0 +1,69 @@ +# 변경 내용 요약 - 경동기업 품목/단가 마이그레이션 Phase 1.0 + +**날짜:** 2026-01-28 +**작업자:** Claude Code +**관련 문서:** docs/plans/kd-items-migration-plan.md + +## 📋 변경 개요 + +경동기업(tenant_id=287) 레거시 DB(chandj)에서 SAM DB(samdb)로 품목/단가 데이터 마이그레이션을 위한 Seeder 생성 + +## 📁 추가된 파일 + +| 파일 | 설명 | +|------|------| +| `api/database/seeders/Kyungdong/KyungdongItemSeeder.php` | 경동기업 품목/단가 마이그레이션 Seeder | + +## 🔧 상세 변경 사항 + +### 1. KyungdongItemSeeder.php 생성 + +**기능:** +- chandj.KDunitprice (601건) → samdb.items 마이그레이션 +- items 기반 → samdb.prices 마이그레이션 +- 기존 tenant_id=287 데이터 삭제 후 재생성 + +**주요 로직:** +```php +// item_div → item_type 매핑 +'[제품]' => 'FG' // 완제품 +'[상품]' => 'FG' // 완제품 +'[반제품]' => 'PT' // 부품 +'[부재료]' => 'SM' // 부자재 +'[원재료]' => 'RM' // 원자재 +'[무형상품]' => 'CS' // 소모품 +``` + +**발견된 이슈 및 해결:** +- 레거시 DB의 `is_deleted` 컬럼이 `0`이 아닌 `NULL`로 활성 상태 표시 +- `where('is_deleted', 0)` → `whereNull('is_deleted')` 수정 + +## ✅ 실행 방법 + +```bash +# Docker 컨테이너 내부에서 실행 +docker exec sam-api-1 php artisan db:seed --class="Database\\Seeders\\Kyungdong\\KyungdongItemSeeder" + +# 또는 Docker 환경에서 직접 실행 +cd /var/www/html && php artisan db:seed --class="Database\\Seeders\\Kyungdong\\KyungdongItemSeeder" +``` + +## 📊 예상 결과 + +| 테이블 | 작업 | 예상 건수 | +|--------|------|----------| +| items | DELETE (기존) | ~10,472건 | +| items | INSERT (신규) | ~601건 | +| prices | DELETE (기존) | ~86건 | +| prices | INSERT (신규) | ~601건 | + +## ⚠️ 주의사항 + +1. **기존 데이터 삭제됨**: tenant_id=287의 모든 items, prices 삭제 +2. **실행 전 백업 권장**: 중요 데이터는 백업 후 실행 +3. **Docker 환경 필수**: chandj DB 연결은 Docker 내부에서만 가능 (sam-mysql-1 호스트명) + +## 🔗 관련 문서 + +- [kd-items-migration-plan.md](../plans/kd-items-migration-plan.md) - 전체 마이그레이션 계획 +- [kd-orders-migration-plan.md](../plans/kd-orders-migration-plan.md) - 입고/재고/주문 마이그레이션 (연관) \ No newline at end of file diff --git a/plans/kd-items-migration-plan.md b/plans/kd-items-migration-plan.md new file mode 100644 index 0000000..cff5e9a --- /dev/null +++ b/plans/kd-items-migration-plan.md @@ -0,0 +1,1251 @@ +# 경동기업(5130) 품목/단가 마이그레이션 계획 + +> **작성일**: 2026-01-28 +> **목적**: 경동기업 레거시 시스템(5130/)의 **품목(items), 단가(prices), BOM** 데이터를 SAM으로 이관 +> **기준 문서**: `5130/` 폴더 분석 결과 +> **상태**: 🔄 분석 완료, 구현 대기 +> **데이터 규모**: ~1,500 레코드 (items ~800 + prices ~500 + BOM ~200) + +--- + +## 🚀 새 세션 시작 가이드 (Quick Start) + +### 이 문서만 보고 작업을 재개하려면: + +```bash +# 1. Docker 서비스 확인 +docker ps | grep sam + +# 2. 레거시 DB (chandj) 접속 테스트 +docker exec sam-mysql-1 mysql -uroot -proot chandj -e "SELECT COUNT(*) FROM KDunitprice;" + +# 3. 현재 진행 상태 확인 +# → 아래 "📍 현재 진행 상태" 섹션 참조 + +# 4. 다음 작업 시작 +# → "📍 현재 진행 상태" > "다음 작업" 참조 +``` + +### 환경 정보 + +| 항목 | 값 | +|------|-----| +| **프로젝트 루트** | `/Users/kent/Works/@KD_SAM/SAM` | +| **레거시 소스** | `5130/` (프로젝트 루트 직하) | +| **API 프로젝트** | `api/` | +| **Docker 컨테이너** | `sam-mysql-1` | +| **레거시 DB** | `chandj` (MySQL) | +| **SAM DB** | `samdb` (MySQL) ⚠️ | +| **대상 테넌트 ID** | `287` (경동기업) | +| **생성자 사용자 ID** | `1` | + +### DB 접속 명령어 + +```bash +# 레거시 DB (chandj) 접속 +docker exec -it sam-mysql-1 mysql -uroot -proot chandj + +# SAM DB 접속 +docker exec -it sam-mysql-1 mysql -uroot -proot samdb + +# 레거시 테이블 목록 확인 +docker exec sam-mysql-1 mysql -uroot -proot chandj -e "SHOW TABLES;" + +# SAM items 테이블 확인 +docker exec sam-mysql-1 mysql -uroot -proot samdb -e "SELECT COUNT(*) FROM items WHERE tenant_id=287;" +``` + +### 전제 조건 (작업 전 확인) + +- [x] Docker 서비스 실행 중 +- [x] `sam-mysql-1` 컨테이너 실행 중 +- [x] chandj 데이터베이스 접근 가능 +- [ ] SAM items 마이그레이션 실행 완료 (`php artisan migrate`) +- [ ] SAM prices 마이그레이션 실행 완료 + +--- + +## 📍 현재 진행 상태 + +| 항목 | 내용 | +|------|------| +| **마지막 완료 작업** | KyungdongItemSeeder.php 생성 완료 ✅ | +| **다음 작업** | Phase 1.0: Seeder 실행 (사용자 승인 필요) | +| **진행률** | 1/4 (25%) - Seeder 생성 완료, 실행 대기 | +| **마지막 업데이트** | 2026-01-28 | + +### 다음 작업 상세 + +**Phase 1.0: Seeder 실행** ⭐ 사용자 승인 후 진행! + +1. **환경 준비**: ✅ 완료 + - 기존 'chandj' DB 연결 사용 (config/database.php) + - 기존 CHANDJ_DB_* 환경변수 사용 (.env) + +2. **Seeder 파일 생성**: ✅ 완료 + - 파일: `api/database/seeders/Kyungdong/KyungdongItemSeeder.php` + - 수정: `is_deleted=0` → `whereNull('is_deleted')` (레거시 데이터 특성 반영) + +3. **실행 전 검증**: + ```bash + # KDunitprice 데이터 확인 (⭐ 실제 컬럼명 사용) + docker exec sam-mysql-1 mysql -uroot -proot chandj -e "SELECT COUNT(*), item_div FROM KDunitprice WHERE is_deleted=0 GROUP BY item_div;" + docker exec sam-mysql-1 mysql -uroot -proot chandj -e "SELECT num, prodcode, item_name, item_div, spec, unit, unitprice FROM KDunitprice WHERE is_deleted=0 LIMIT 5;" + ``` + +4. **Seeder 실행**: + ```bash + cd /Users/kent/Works/@KD_SAM/SAM/api + php artisan db:seed --class=Database\\Seeders\\Kyungdong\\KyungdongItemSeeder + ``` + +5. **결과 확인**: + ```bash + docker exec sam-mysql-1 mysql -uroot -proot samdb -e "SELECT item_type, COUNT(*) FROM items WHERE tenant_id=287 GROUP BY item_type;" + docker exec sam-mysql-1 mysql -uroot -proot samdb -e "SELECT COUNT(*) FROM prices WHERE tenant_id=287;" + ``` + +6. ⚠️ **실행 전 사용자 승인 필요** + +--- + +## 0. 성공 기준 + +| 기준 | 목표값 | 확인 방법 | +|------|-------|----------| +| **items 합계** | **~800건** | `SELECT COUNT(*) FROM items WHERE tenant_id=287` | +| items (FG) | ~100건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='FG'` | +| items (PT) | ~250건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='PT'` | +| items (SM) | ~300건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='SM'` | +| items (RM) | ~100건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='RM'` | +| items (CS) | ~50건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='CS'` | +| **prices 합계** | **~500건** | `SELECT COUNT(*) FROM prices WHERE tenant_id=287` | +| **BOM 관계** | ~300건 | `SELECT COUNT(*) FROM item_bom_items WHERE tenant_id=287` | +| code 유일성 | 100% | `SELECT code, COUNT(*) FROM items WHERE tenant_id=287 GROUP BY code HAVING COUNT(*) > 1` (0건) | +| API 테스트 | 100% | `/api/v1/items` 목록 조회 성공 | + +--- + +## 1. 개요 + +### 1.1 배경 + +경동기업은 **모델(Model) + 제품(Product)** 분리 구조를 사용하지만, SAM은 **통합 items 테이블** 구조로 기획됨. 레거시 5130/ 폴더의 데이터를 SAM 구조에 맞게 변환하여 이관 필요. + +### 1.2 핵심 차이점 + +``` +┌────────────────────────────────────────────────────────────────────────────┐ +│ 레거시 (chandj) → SAM (samdb) │ +├────────────────────────────────────────────────────────────────────────────┤ +│ 📦 품목 마스터 │ +│ ───────────────────────────────────────────────────────────────────────── │ +│ KDunitprice (603건, 핵심!) → items (마스터, code로 구분) │ +│ models (18건) → items (FG) │ +│ parts, parts_sub (170건) → item_bom_items │ +│ category_l1~l4 → items 카테고리 참조 │ +│ guiderail, bottombar, bending 등 → item_details │ +│ │ +│ 💰 단가 정보 │ +│ ───────────────────────────────────────────────────────────────────────── │ +│ price_* (10개 테이블) → prices │ +│ KDunitprice.출고가/입고가 → prices (기본가) │ +└────────────────────────────────────────────────────────────────────────────┘ +``` + +### 1.2.1 중복 제거 전략 ⭐ + +``` +┌────────────────────────────────────────────────────────────────────────────┐ +│ 🎯 핵심: KDunitprice가 마스터, code 필드로 중복 방지 │ +├────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 1️⃣ KDunitprice (603건) → items 먼저 생성 │ +│ - item_div로 item_type 결정 │ +│ - code = prodcode 그대로 사용 ⭐ │ +│ │ +│ 2️⃣ price_* 테이블 → items 중복 확인 후 prices만 생성 │ +│ - code로 items 조회 │ +│ - 존재하면 → prices만 추가 (item_id 연결) │ +│ - 없으면 → items 생성 후 prices 추가 │ +│ │ +│ 3️⃣ 매핑 테이블 불필요 │ +│ - item_id_mappings ❌ (양방향 조회 불필요) │ +│ - chandj는 손 안댐, samdb에만 밀어 넣음 │ +│ │ +└────────────────────────────────────────────────────────────────────────────┘ +``` + +### 1.3 SAM items 구조 (Target) + +```sql +-- items 테이블 (tenant_id=287 for 경동기업) +-- ⚠️ 실제 컬럼명 (2026-01-28 확인됨) +CREATE TABLE items ( + id BIGINT PRIMARY KEY, + tenant_id BIGINT NOT NULL, -- 287 (경동기업) + item_type VARCHAR(15) NOT NULL, -- FG, PT, SM, RM, CS + code VARCHAR(100) NOT NULL, -- 품목코드 (← KDunitprice.prodcode) + name VARCHAR(255) NOT NULL, -- 품목명 (← KDunitprice.item_name) + unit VARCHAR(20), -- 단위 (← KDunitprice.unit) + category_id BIGINT, -- 카테고리 ID + process_type VARCHAR(50), -- 공정 타입 + item_category VARCHAR(50), -- 품목 분류 + bom JSON, -- BOM 정보 + attributes JSON, -- 동적 필드 값 (spec 등) + attributes_archive JSON, -- 속성 아카이브 + options JSON, -- 추가 옵션 + description TEXT, -- 설명 + is_active BOOLEAN DEFAULT TRUE, + created_by BIGINT, + updated_by BIGINT, + deleted_by BIGINT, + created_at TIMESTAMP, + updated_at TIMESTAMP, + deleted_at TIMESTAMP -- Soft Delete +); +``` + +### 1.4 item_type 분류 + +| SAM item_type | 설명 | 레거시 소스 | +|---------------|------|-------------| +| **FG** | 완제품 (Finished Goods) | KDunitprice[제품], KDunitprice[상품], models | +| **PT** | 부품 (Parts) | KDunitprice[반제품], parts, parts_sub, category_l4 | +| **SM** | 부자재 (Sub-Materials) | KDunitprice[부재료], price_* 테이블들 | +| **RM** | 원자재 (Raw Materials) | KDunitprice[원재료], price_raw_materials | +| **CS** | 소모품 (Consumables) | KDunitprice[무형상품], 기타 | + +### 1.4.1 KDunitprice.item_div → item_type 매핑 ⭐ + +```sql +-- KDunitprice.item_div 값 목록 (603건 중) +-- [제품], [상품], [부재료], [원재료], [반제품], [무형상품] + +CASE item_div + WHEN '[제품]' THEN 'FG' -- 완제품 + WHEN '[상품]' THEN 'FG' -- 상품도 완제품으로 분류 + WHEN '[반제품]' THEN 'PT' -- 반제품 = 부품 + WHEN '[부재료]' THEN 'SM' -- 부자재 + WHEN '[원재료]' THEN 'RM' -- 원자재 + WHEN '[무형상품]' THEN 'CS' -- 소모품/무형 + ELSE 'SM' -- 기본값 +END AS item_type +``` + +--- + +## 2. 레거시 DB 구조 분석 + +### 2.1 핵심 테이블 및 레코드 수 + +#### 📦 품목 마스터 테이블 + +| 테이블 | 레코드 수 | 역할 | SAM 매핑 | +|--------|----------|------|----------| +| **`KDunitprice`** ⭐ | **603** | **품목 마스터 (핵심!)** | **items (마스터)** | +| `models` | 18 | 모델 마스터 (스크린/철재) | items (FG) | +| `BDmodels` | 59 | 모델별 BOM + 단가 (JSON) | item_bom_items + prices | +| `parts` | 36 | 부품 | item_bom_items | +| `parts_sub` | 134 | 하위 부품 | item_bom_items | +| `category_l1` | 2 | 1단계 카테고리 (스크린/철재) | 참조용 | +| `category_l2` | 14 | 2단계 카테고리 | 참조용 | +| `category_l3` | 24 | 3단계 카테고리 | 참조용 | +| `category_l4` | 37 | 4단계 카테고리 (부품) | items (PT) | +| `item_list` | 5+ | 품목 마스터 | items (PT) | + +#### 💰 단가 테이블 + +| 테이블 | 레코드 수 | 역할 | SAM 매핑 | +|--------|----------|------|----------| +| `price_motor` | 2 (JSON) | 모터 단가 | prices | +| `price_shaft` | 2 (JSON) | 감기샤프트 단가 | prices | +| `price_pipe` | 2 (JSON) | 파이프 단가 | prices | +| `price_angle` | 2 (JSON) | 앵글 단가 | prices | +| `price_raw_materials` | 6 (JSON) | 주자재 단가 | prices | +| `price_bend` | 3 (JSON) | 절곡 단가 | prices | +| `price_pole` | 2 (JSON) | 폴 단가 | prices | +| `price_screenplate` | 2 (JSON) | 스크린플레이트 단가 | prices | +| `price_smokeban` | 2 (JSON) | 연기차단 단가 | prices | + +### 2.2 KDunitprice 테이블 구조 ⭐ (핵심 마스터) + +```sql +-- KDunitprice: 품목 마스터 (603건) - 가장 중요한 테이블! +-- ⚠️ 실제 컬럼명 (2026-01-28 확인됨) +num INT PRIMARY KEY, -- PK +is_deleted INT, -- 삭제 여부 +prodcode VARCHAR(50), -- items.code (유니크 키!) ⭐ +item_name VARCHAR(255), -- items.name ⭐ +item_div VARCHAR(20), -- [제품]/[상품]/[부재료]/[원재료]/[반제품]/[무형상품] → item_type ⭐ +spec VARCHAR(100), -- items.attributes.spec +unit VARCHAR(20), -- items.unit +unitprice DECIMAL, -- prices.sales_price (단일 컬럼, 입고가/출고가 구분 없음!) ⭐ +searchtag TEXT, -- 검색 태그 +update_log TEXT -- 변경 이력 +``` + +**item_div 분포 확인 쿼리**: +```sql +SELECT item_div, COUNT(*) FROM KDunitprice WHERE is_deleted=0 GROUP BY item_div; +-- [제품] ~100건 → FG +-- [상품] ~50건 → FG +-- [반제품] ~100건 → PT +-- [부재료] ~200건 → SM +-- [원재료] ~100건 → RM +-- [무형상품] ~53건 → CS +``` + +### 2.3 BDmodels 테이블 구조 (BOM + 단가) + +```sql +-- BDmodels: 모델별 BOM 및 단가 정보 +num INT PRIMARY KEY, +major_category VARCHAR(10), -- 스크린/철재 +spec VARCHAR(30), -- 규격 (60*40, 120*70 등) +model_name VARCHAR(255), -- 모델명 +finishing_type ENUM('SUS마감','EGI마감'), +check_type VARCHAR(20), -- 벽면형/측면형/혼합형 +seconditem VARCHAR(30), -- 부품명 (가이드레일, 하단마감재, L-BAR 등) +unitprice TEXT, -- 단가 (문자열) +savejson TEXT, -- BOM 상세 JSON +description TEXT, +is_deleted, priceDate DATE +``` + +**savejson 예시** (가이드레일 BOM): +```json +[ + {"col1":"1번(마감재)","col2":"SUS 1.2T","col3":"-3","col4":"203","col5":"206","col6":"51,000","col7":"10,506","col8":"2","col9":"21,012","col10":"삭제"}, + {"col1":"2번(본체)","col2":"EGI 1.55T","col3":"-5","col4":"294","col5":"299","col6":"27,000","col7":"8,073","col8":"1","col9":"8,073","col10":"삭제"} +] +``` + +### 2.4 단가 시스템 상세 분석 ⭐ + +#### 2.4.1 레거시 단가 테이블 전체 목록 (10개) + +| 테이블명 | 레코드 수 | 최신 날짜 | 용도 | +|---------|----------|----------|------| +| `price_motor` | 2 | 2024-08-25 | 전동개폐기, 제어기 단가 | +| `price_shaft` | 2 | 2024-08-25 | 감기샤프트 단가 | +| `price_pipe` | 2 | 2024-08-26 | 파이프 단가 | +| `price_angle` | 2 | 2024-08-26 | 앵글 단가 | +| `price_raw_materials` | 6 | 2025-06-18 | 슬랫, 스크린 원자재 단가 | +| `price_bend` | 3 | 2025-03-09 | 절곡 단가 | +| `price_pole` | 2 | 2024-08-26 | 폴 단가 | +| `price_screenplate` | 2 | 2024-08-26 | 스크린플레이트 단가 | +| `price_smokeban` | 2 | 2024-08-26 | 연기차단 단가 | +| `price_etc` | 0 | - | 기타 (개별 컬럼 방식, 비활성) | + +#### 2.4.2 SAM prices 테이블 구조 (Target) + +```sql +CREATE TABLE prices ( + id BIGINT PRIMARY KEY, + tenant_id BIGINT NOT NULL, -- 287 (경동기업) + + -- 품목 연결 + item_type_code VARCHAR(20), -- FG/PT/SM/RM/CS + item_id BIGINT, -- items.id FK + client_group_id BIGINT NULL, -- NULL = 기본가 + + -- 원가 정보 + purchase_price DECIMAL(15,4), -- 매입단가 (원가) + processing_cost DECIMAL(15,4), -- 가공비 + loss_rate DECIMAL(5,2), -- LOSS율 (%) + + -- 판매가 정보 + margin_rate DECIMAL(5,2), -- 마진율 (%) + sales_price DECIMAL(15,4), -- 판매단가 ⭐ + rounding_rule ENUM('round','ceil','floor'), + rounding_unit INT DEFAULT 1, -- 반올림 단위 + + -- 메타 정보 + supplier VARCHAR(255), -- 공급업체 + effective_from DATE, -- 적용 시작일 ⭐ + effective_to DATE NULL, -- 적용 종료일 + note TEXT, + + -- 상태 관리 + status ENUM('draft','active','inactive','finalized'), + is_final BOOLEAN DEFAULT FALSE, + + -- 감사 컬럼 + created_by, updated_by, deleted_by, timestamps, soft_deletes +); +``` + +--- + +## 3. 매핑 설계 + +### 3.1 models → items (FG 완제품) + +| 레거시 (models) | SAM (items) | 비고 | +|----------------|-------------|------| +| model_id | (신규 생성) | | +| model_name | code | KSS01 → FG-KSS01 | +| - | name | 모델명 + 마감타입 + 가이드타입 조합 | +| major_category | attributes.major_category | 스크린/철재 | +| finishing_type | attributes.finishing_type | SUS마감/EGI마감 | +| guiderail_type | attributes.guiderail_type | 벽면형/측면형/혼합형 | +| - | item_type | 'FG' | +| - | tenant_id | 287 | + +**코드 생성 규칙**: +``` +FG-{model_name}-{guiderail_type}-{finishing_type} +예: FG-KSS01-벽면형-SUS +``` + +### 3.2 price_* → prices 테이블 (단가 연동) ⭐ + +> **중요**: 단가 데이터는 items.attributes가 아닌 **prices 테이블**에 별도 관리 + +| 레거시 (price_*) | SAM (prices) | 비고 | +|-----------------|--------------|------| +| registedate | effective_from | 적용 시작일 | +| itemList.col13 (판매가) | sales_price | | +| itemList.col11 (원가) | purchase_price | | +| - | item_type_code | FG/PT/SM/RM/CS | +| - | item_id | items.id FK | +| - | client_group_id | NULL (기본가) | +| - | status | 'active' | + +--- + +## 4. 대상 범위 + +### 4.1 Phase 1: 마스터 데이터 이관 + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| **1.0** | **KDunitprice → items (마스터)** ⭐ | ⏳ | **603건 (최우선!)** | +| 1.1 | models → items (FG) INSERT 쿼리 작성 | ⏳ | 18건 (중복 확인 후) | +| 1.2 | item_list → items (PT) INSERT 쿼리 작성 | ⏳ | 5건+ (중복 확인 후) | +| 1.3 | category_l4 → items (PT) INSERT 쿼리 작성 | ⏳ | 37건 (중복 확인 후) | +| 1.4 | price_motor 파싱 → prices 연결 | ⏳ | code로 items 조회 후 prices 생성 | +| 1.5 | price_shaft 파싱 → prices 연결 | ⏳ | ~15건 | +| 1.6 | price_raw_materials 파싱 → prices 연결 | ⏳ | ~20건 | +| 1.7 | ⚠️ **사용자 승인**: Phase 1 INSERT 실행 | ⏳ | | + +### 4.2 Phase 2: BOM 및 상세 데이터 이관 + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 2.1 | BDmodels.savejson → item_bom_items | ⏳ | 59건 | +| 2.2 | parts → item_bom_items | ⏳ | 36건 | +| 2.3 | parts_sub → item_bom_items | ⏳ | 134건 | +| 2.4 | guiderail/bottombar/bending 등 → item_details | ⏳ | 제품 상세 | +| 2.5 | parent_item_id, child_item_id 매핑 | ⏳ | code 기반 조회 | + +### 4.3 Phase 3: 단가 데이터 이관 ⭐ + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 3.1 | price_motor → items (SM) + prices | ⏳ | ~35건 품목 + 단가 | +| 3.2 | price_shaft → items (SM) + prices | ⏳ | ~15건 | +| 3.3 | price_pipe → items (SM) + prices | ⏳ | ~10건 | +| 3.4 | price_angle → items (SM) + prices | ⏳ | ~10건 | +| 3.5 | price_raw_materials → items (RM) + prices | ⏳ | ~20건 | +| 3.6 | price_bend → items (SM) + prices | ⏳ | ~10건 | +| 3.7 | price_pole → items (SM) + prices | ⏳ | ~5건 | +| 3.8 | price_screenplate → items (SM) + prices | ⏳ | ~5건 | +| 3.9 | price_smokeban → items (SM) + prices | ⏳ | ~5건 | +| 3.10 | 단가 버전 이력 정리 | ⏳ | effective_from/to 설정 | +| 3.11 | ⚠️ **사용자 승인**: 단가 INSERT 실행 | ⏳ | | + +### 4.4 Phase 4: 검증 및 배포 + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 4.1 | 로컬 테스트 | ⏳ | | +| 4.2 | API 테스트 | ⏳ | | +| 4.3 | 개발서버 배포 | ⏳ | ⚠️ 컨펌 필요 | + +--- + +## 5. Seeder 파일 + +### 5.0 Seeder 구조 및 실행 방법 + +**파일 위치**: `api/database/seeders/Kyungdong/KyungdongItemSeeder.php` + +**실행 명령어**: +```bash +# 로컬 실행 (tenant_id=287만 삭제 후 INSERT) +cd /Users/kent/Works/@KD_SAM/SAM/api +php artisan db:seed --class=Database\\Seeders\\Kyungdong\\KyungdongItemSeeder + +# 개발서버 실행 (TRUNCATE 후 INSERT) - ⚠️ 컨펌 필요 +php artisan db:seed --class=Database\\Seeders\\Kyungdong\\KyungdongItemSeeder --env=development +``` + +**환경별 삭제 전략**: +| 환경 | 삭제 방식 | 비고 | +|------|----------|------| +| 로컬 (local) | `DELETE WHERE tenant_id=287` | 다른 테넌트 데이터 보존 | +| 개발 (development) | `TRUNCATE` | 전체 초기화 | + +--- + +### 5.1 KyungdongItemSeeder.php (전체 코드) + +```php +command->info('🚀 경동기업 품목/단가 마이그레이션 시작...'); + + // 1. 기존 데이터 삭제 + $this->cleanupExistingData(); + + // 2. KDunitprice → items + $itemCount = $this->migrateItems(); + + // 3. KDunitprice → prices + $priceCount = $this->migratePrices(); + + $this->command->info("✅ 완료: items {$itemCount}건, prices {$priceCount}건"); + } + + /** + * 기존 데이터 삭제 + */ + private function cleanupExistingData(): void + { + if (App::environment('local')) { + // 로컬: tenant_id=287만 삭제 + $this->command->info(' 🧹 로컬 환경: tenant_id=287 데이터 삭제...'); + DB::table('prices')->where('tenant_id', self::TENANT_ID)->delete(); + DB::table('items')->where('tenant_id', self::TENANT_ID)->delete(); + } else { + // 개발/운영: TRUNCATE (⚠️ 주의) + $this->command->info(' 🧹 개발 환경: TRUNCATE...'); + DB::statement('SET FOREIGN_KEY_CHECKS=0'); + DB::table('prices')->truncate(); + DB::table('items')->truncate(); + DB::statement('SET FOREIGN_KEY_CHECKS=1'); + } + } + + /** + * KDunitprice → items 마이그레이션 + */ + private function migrateItems(): int + { + $this->command->info(' 📦 KDunitprice → items 마이그레이션...'); + + // chandj.KDunitprice에서 데이터 조회 + $kdItems = DB::connection('legacy') // config/database.php에 'legacy' 연결 필요 + ->table('KDunitprice') + ->where('is_deleted', 0) + ->whereNotNull('prodcode') + ->where('prodcode', '!=', '') + ->get(); + + $items = []; + $now = now(); + + foreach ($kdItems as $kd) { + $items[] = [ + 'tenant_id' => self::TENANT_ID, + 'item_type' => $this->mapItemType($kd->item_div), + 'code' => $kd->prodcode, + 'name' => $kd->item_name, + 'unit' => $kd->unit, + 'attributes' => json_encode([ + 'spec' => $kd->spec, + 'item_div' => $kd->item_div, + 'legacy_source' => 'KDunitprice', + 'legacy_num' => $kd->num, + ]), + 'is_active' => true, + 'created_by' => self::USER_ID, + 'updated_by' => self::USER_ID, + 'created_at' => $now, + 'updated_at' => $now, + ]; + + // 500건씩 배치 INSERT + if (count($items) >= 500) { + DB::table('items')->insert($items); + $items = []; + } + } + + // 남은 데이터 INSERT + if (!empty($items)) { + DB::table('items')->insert($items); + } + + return $kdItems->count(); + } + + /** + * KDunitprice → prices 마이그레이션 + */ + private function migratePrices(): int + { + $this->command->info(' 💰 KDunitprice → prices 마이그레이션...'); + + // items와 KDunitprice 조인하여 prices 생성 + $count = DB::statement(" + INSERT INTO prices ( + tenant_id, item_type_code, item_id, client_group_id, + purchase_price, sales_price, + effective_from, status, + created_by, updated_by, created_at, updated_at + ) + SELECT + ? AS tenant_id, + i.item_type AS item_type_code, + i.id AS item_id, + NULL AS client_group_id, + 0 AS purchase_price, + COALESCE(k.unitprice, 0) AS sales_price, + CURDATE() AS effective_from, + 'active' AS status, + ? AS created_by, + ? AS updated_by, + NOW(), NOW() + FROM items i + JOIN " . config('database.connections.legacy.database') . ".KDunitprice k + ON k.prodcode = i.code + WHERE i.tenant_id = ? + AND k.is_deleted = 0 + AND k.prodcode IS NOT NULL + AND k.prodcode != '' + ", [self::TENANT_ID, self::USER_ID, self::USER_ID, self::TENANT_ID]); + + return DB::table('prices')->where('tenant_id', self::TENANT_ID)->count(); + } + + /** + * item_div → item_type 매핑 + */ + private function mapItemType(?string $itemDiv): string + { + return match ($itemDiv) { + '[제품]', '[상품]' => 'FG', + '[반제품]' => 'PT', + '[부재료]' => 'SM', + '[원재료]' => 'RM', + '[무형상품]' => 'CS', + default => 'SM', + }; + } +} +``` + +--- + +### 5.2 Legacy DB 연결 설정 + +**config/database.php에 추가**: +```php +'connections' => [ + // ... 기존 연결들 + + 'legacy' => [ + 'driver' => 'mysql', + 'host' => env('LEGACY_DB_HOST', '127.0.0.1'), + 'port' => env('LEGACY_DB_PORT', '3306'), + 'database' => env('LEGACY_DB_DATABASE', 'chandj'), + 'username' => env('LEGACY_DB_USERNAME', 'root'), + 'password' => env('LEGACY_DB_PASSWORD', 'root'), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + ], +], +``` + +**.env에 추가**: +```env +LEGACY_DB_HOST=127.0.0.1 +LEGACY_DB_PORT=3306 +LEGACY_DB_DATABASE=chandj +LEGACY_DB_USERNAME=root +LEGACY_DB_PASSWORD=root +``` + +--- + +### 5.3 참고: SQL 쿼리 (직접 실행용) + +#### 5.3.1 KDunitprice → items (마스터) + +```sql +-- ⚠️ 참고용 SQL (Seeder 사용 권장) +-- KDunitprice: 품목 마스터 (603건) → SAM items + +INSERT INTO samdb.items ( + tenant_id, item_type, code, name, unit, + attributes, description, is_active, + created_by, created_at, updated_at +) +SELECT + 287 AS tenant_id, + -- item_div → item_type 매핑 + CASE item_div + WHEN '[제품]' THEN 'FG' + WHEN '[상품]' THEN 'FG' + WHEN '[반제품]' THEN 'PT' + WHEN '[부재료]' THEN 'SM' + WHEN '[원재료]' THEN 'RM' + WHEN '[무형상품]' THEN 'CS' + ELSE 'SM' + END AS item_type, + prodcode AS code, -- 유니크 키! ⭐ + item_name AS name, -- ⭐ + unit AS unit, + JSON_OBJECT( + 'spec', spec, -- ⭐ + 'item_div', item_div, + 'legacy_source', 'KDunitprice', + 'legacy_num', num + ) AS attributes, + NULL AS description, -- 비고 컬럼 없음 + 1 AS is_active, + 1 AS created_by, + NOW(), NOW() +FROM chandj.KDunitprice +WHERE is_deleted = 0 + AND prodcode IS NOT NULL AND prodcode != ''; + +-- 결과 확인 +SELECT item_type, COUNT(*) +FROM samdb.items +WHERE tenant_id = 287 +GROUP BY item_type; +``` + +#### 5.3.2 KDunitprice → prices (기본 단가) + +```sql +-- ⚠️ 참고용 SQL (Seeder 사용 권장) +-- unitprice 단일 컬럼 → sales_price, purchase_price는 0 +INSERT INTO samdb.prices ( + tenant_id, item_type_code, item_id, client_group_id, + purchase_price, sales_price, + effective_from, status, + created_by, created_at, updated_at +) +SELECT + 287 AS tenant_id, + i.item_type AS item_type_code, + i.id AS item_id, + NULL AS client_group_id, -- 기본가 + 0 AS purchase_price, -- 입고가 컬럼 없음, 0으로 설정 + COALESCE(k.unitprice, 0) AS sales_price, -- ⭐ unitprice 사용 + CURDATE() AS effective_from, -- 적용일 + 'active' AS status, + 1 AS created_by, + NOW(), NOW() +FROM chandj.KDunitprice k +JOIN samdb.items i ON i.code = k.prodcode AND i.tenant_id = 287 -- ⭐ prodcode 사용 +WHERE k.is_deleted = 0 + AND k.prodcode IS NOT NULL AND k.prodcode != ''; +``` + +### 5.4 models → items (FG) - 추가 SQL 참고용 + +```sql +-- ⚠️ 참고용 SQL (Seeder 확장 시 사용) +-- 레거시 chandj.models → SAM items (FG) +-- KDunitprice에 없는 것만 추가 (중복 확인 필요) +INSERT INTO samdb.items ( + tenant_id, item_type, code, name, unit, + attributes, is_active, created_by, created_at, updated_at +) +SELECT + 287 AS tenant_id, + 'FG' AS item_type, + CONCAT('FG-', model_name, '-', + COALESCE(guiderail_type, 'STD'), '-', + CASE finishing_type + WHEN 'SUS마감' THEN 'SUS' + WHEN 'EGI마감' THEN 'EGI' + ELSE 'STD' + END + ) AS code, + CONCAT(model_name, ' ', major_category, ' ', finishing_type, ' ', COALESCE(guiderail_type, '')) AS name, + 'EA' AS unit, + JSON_OBJECT( + 'major_category', major_category, + 'finishing_type', finishing_type, + 'guiderail_type', guiderail_type, + 'legacy_model_id', model_id + ) AS attributes, + CASE WHEN is_deleted = 0 THEN 1 ELSE 0 END AS is_active, + 1 AS created_by, + created_at, + updated_at +FROM chandj.models +WHERE is_deleted = 0; +``` + +### 5.5 category_l4 → items (PT) - 추가 SQL 참고용 + +```sql +-- ⚠️ 참고용 SQL (Seeder 확장 시 사용) +-- 레거시 4단계 카테고리 → SAM items (PT) +INSERT INTO samdb.items ( + tenant_id, item_type, code, name, unit, + attributes, is_active, created_by, created_at, updated_at +) +SELECT + 287 AS tenant_id, + 'PT' AS item_type, + CONCAT('PT-', l1.name, '-', l2.name, '-', l3.name, '-', l4.name) AS code, + l4.name AS name, + 'EA' AS unit, + JSON_OBJECT( + 'category_l1', l1.name, + 'category_l2', l2.name, + 'category_l3', l3.name, + 'category_l4', l4.name, + 'legacy_l4_id', l4.id + ) AS attributes, + 1 AS is_active, + 1 AS created_by, + NOW(), NOW() +FROM chandj.category_l4 l4 +JOIN chandj.category_l3 l3 ON l4.parent_id = l3.id +JOIN chandj.category_l2 l2 ON l3.parent_id = l2.id +JOIN chandj.category_l1 l1 ON l2.parent_id = l1.id; +``` + +### 5.6 price_motor → items (SM) + prices - PHP 스크립트 참고용 + +```php +query(" + SELECT num, registedate, itemList + FROM price_motor + WHERE is_deleted = 0 + ORDER BY registedate DESC +"); +$priceRecords = $stmt->fetchAll(PDO::FETCH_ASSOC); + +// 최신 단가의 itemList 파싱 → items 생성 +$latestRecord = $priceRecords[0]; +$itemList = json_decode($latestRecord['itemList'], true); + +foreach ($itemList as $idx => $item) { + $voltage = $item['col1']; // 220, 380, 제어기, 방화, 방범 + $capacity = $item['col2']; // 150K(S), 300K, 노출형, 매립형... + $purchasePrice = (float)str_replace(',', '', $item['col11'] ?? '0'); + $salesPrice = (float)str_replace(',', '', $item['col13'] ?? '0'); + + // 품목 코드 생성 + $code = "SM-MOTOR-" . preg_replace('/[^A-Za-z0-9가-힣]/', '', $voltage) + . "-" . preg_replace('/[^A-Za-z0-9가-힣()]/', '', $capacity); + + // 품목명 생성 + if (in_array($voltage, ['220', '380'])) { + $name = "전동개폐기 {$voltage}V {$capacity}"; + $itemType = 'SM'; + } elseif ($voltage === '제어기') { + $name = "연동제어기 {$capacity}"; + $itemType = 'SM'; + } else { + $name = "{$voltage} {$capacity}"; + $itemType = 'SM'; + } + + // 1단계: items INSERT + $itemStmt = $pdo->prepare(" + INSERT INTO items ( + tenant_id, item_type, code, name, unit, + attributes, is_active, created_by, created_at, updated_at + ) VALUES (?, ?, ?, ?, 'EA', ?, 1, ?, NOW(), NOW()) + ON DUPLICATE KEY UPDATE name = VALUES(name) + "); + $attributes = json_encode([ + 'voltage' => $voltage, + 'capacity' => $capacity, + 'legacy_source' => 'price_motor', + 'legacy_col_index' => $idx + ]); + $itemStmt->execute([$tenantId, $itemType, $code, $name, $attributes, $userId]); + $itemId = $pdo->lastInsertId(); + + // 2단계: prices INSERT (모든 버전) + foreach ($priceRecords as $priceIdx => $priceRecord) { + $priceItemList = json_decode($priceRecord['itemList'], true); + if (!isset($priceItemList[$idx])) continue; + + $priceItem = $priceItemList[$idx]; + $pPrice = (float)str_replace(',', '', $priceItem['col11'] ?? '0'); + $sPrice = (float)str_replace(',', '', $priceItem['col13'] ?? '0'); + $effectiveFrom = $priceRecord['registedate']; + + // 다음 레코드가 있으면 effective_to 설정 + $effectiveTo = isset($priceRecords[$priceIdx + 1]) + ? date('Y-m-d', strtotime($effectiveFrom . ' -1 day')) + : null; + + $status = ($priceIdx === 0) ? 'active' : 'inactive'; + + $priceStmt = $pdo->prepare(" + INSERT INTO prices ( + tenant_id, item_type_code, item_id, client_group_id, + purchase_price, sales_price, effective_from, effective_to, + status, created_by, created_at, updated_at + ) VALUES (?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, NOW(), NOW()) + "); + $priceStmt->execute([ + $tenantId, $itemType, $itemId, + $pPrice, $sPrice, $effectiveFrom, $effectiveTo, + $status, $userId + ]); + } + + echo "✓ {$code} - items + prices 생성 완료\n"; +} +``` + +--- + +## 6. 기준 원칙 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 🎯 핵심 원칙 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 📦 데이터 전략 │ +│ ───────────────────────────────────────────────────────────────────── │ +│ - KDunitprice(603건)가 품목 마스터 → items 최우선 생성 │ +│ - code 필드로 중복 방지 (ON DUPLICATE KEY UPDATE) │ +│ - BOM은 item_bom_items 테이블 사용 (items.bom JSON ❌) │ +│ - 단가 정보는 prices 테이블에 별도 저장 (items.attributes ❌) │ +│ │ +│ ❌ 불필요한 것 │ +│ ───────────────────────────────────────────────────────────────────── │ +│ - item_id_mappings 테이블 (양방향 조회 불필요) │ +│ - chandj 수정 (손 안댐, samdb에만 밀어 넣음) │ +│ - 레거시 소스 확인 (마이그레이션 후 검증만) │ +│ │ +│ ✅ 필수 사항 │ +│ ───────────────────────────────────────────────────────────────────── │ +│ - 경동기업 기준으로 맞춤 (이미 사용중인 시스템) │ +│ - 전체 이관 (items + prices + BOM) │ +│ - SQL 쿼리 + PHP 스크립트 혼용 (JSON 파싱 필요) │ +│ - 로컬 검증 완료 후 개발서버 배포 │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 6.1 변경 승인 정책 + +| 분류 | 예시 | 승인 | +|------|------|------| +| ✅ 즉시 가능 | SELECT 쿼리, 분석, 매핑 설계 | 불필요 | +| ⚠️ 컨펌 필요 | INSERT 실행, TRUNCATE, 개발서버 배포 | **필수** | +| 🔴 금지 | 운영서버 직접 작업 | 별도 협의 | + +--- + +## 7. 데이터 규모 예상 + +### 7.1 items 테이블 예상 + +| 소스 | 레코드 수 | SAM item_type | 예상 items 건수 | +|------|----------|---------------|----------------| +| **KDunitprice** ⭐ | **603** | FG/PT/SM/RM/CS | **~603 (마스터)** | +| models | 18 | FG | ~0 (중복 제외) | +| category_l4 | 37 | PT | ~20 (일부 신규) | +| item_list | 5 | PT | ~0 (중복 제외) | +| price_* 테이블 | ~130 항목 | SM/RM | ~100 (신규만) | +| **items 합계** | - | - | **~700~800건** | + +**item_type별 분포 예상**: +| item_type | 설명 | 예상 건수 | +|-----------|------|----------| +| FG | 완제품 | ~100건 | +| PT | 부품 | ~250건 | +| SM | 부자재 | ~300건 | +| RM | 원자재 | ~100건 | +| CS | 소모품 | ~50건 | + +### 7.2 prices 테이블 예상 ⭐ + +| 소스 | 버전 수 | 품목당 단가 | 예상 prices 건수 | +|------|--------|------------|-----------------| +| KDunitprice | 1 | 603 | ~603 | +| price_motor | 2 | 35 | ~70 | +| price_shaft | 2 | 15 | ~30 | +| price_pipe | 2 | 10 | ~20 | +| price_angle | 2 | 10 | ~20 | +| price_raw_materials | 6 | 20 | ~120 | +| price_bend | 3 | 10 | ~30 | +| 기타 price_* | 2 | 15 | ~30 | +| **prices 합계** | - | - | **~500건** (중복 제외) | + +--- + +## 8. 체크리스트 + +### Phase 1: 마스터 데이터 이관 +- [x] 레거시 DB 구조 분석 완료 +- [x] KDunitprice 테이블 발견 및 분석 (603건, 핵심 마스터) +- [x] 중복 제거 전략 수립 (code 기반, 매핑 테이블 불필요) +- [x] Seeder 기반 마이그레이션 계획 수립 +- [x] ~~config/database.php에 'legacy' 연결 추가~~ → 기존 'chandj' 연결 사용 +- [x] ~~.env에 LEGACY_DB_* 환경변수 추가~~ → 기존 CHANDJ_DB_* 사용 +- [x] **KyungdongItemSeeder.php 파일 생성** ✅ (2026-01-28) +- [ ] ⚠️ **사용자 승인**: Seeder 실행 + +### Phase 2: BOM 데이터 이관 +- [ ] BDmodels.savejson 파싱 로직 작성 +- [ ] child_item_id 매핑 테이블 생성 +- [ ] items.bom JSON 생성 +- [ ] ⚠️ **사용자 승인**: BOM 데이터 INSERT 실행 + +### Phase 3: 단가 데이터 이관 ⭐ +- [x] 레거시 price_* 테이블 구조 분석 (10개) +- [x] 각 테이블별 JSON 스키마 분석 +- [x] SAM prices 테이블 구조 확인 +- [x] Legacy → SAM 단가 매핑 전략 수립 +- [ ] price_motor → prices 연결 스크립트 작성 +- [ ] price_shaft → prices 연결 스크립트 작성 +- [ ] price_pipe → prices 연결 스크립트 작성 +- [ ] price_angle → prices 연결 스크립트 작성 +- [ ] price_raw_materials → prices 연결 스크립트 작성 +- [ ] 기타 price_* 테이블 처리 +- [ ] 단가 버전 이력 정리 (effective_from/to) +- [ ] ⚠️ **사용자 승인**: 단가 INSERT 실행 + +### Phase 4: 검증 및 배포 +- [ ] 건수 검증 +- [ ] API 테스트 +- [ ] ⚠️ **사용자 승인**: 개발서버 배포 + +--- + +## 9. 참고 문서 + +- **레거시 소스**: `5130/` 폴더 +- **SAM items 마이그레이션**: `api/database/migrations/2025_12_13_152507_create_items_table.php` +- **SAM prices 마이그레이션**: `api/database/migrations/2025_12_08_154633_create_prices_table.php` +- **SAM price_revisions 마이그레이션**: `api/database/migrations/2025_12_08_154634_create_price_revisions_table.php` +- **품목 분석**: `docs/data/analysis/item-db-analysis.md` +- **DummyItemSeeder**: `api/database/seeders/Dummy/DummyItemSeeder.php` +- **DummyDataSeeder**: `api/database/seeders/DummyDataSeeder.php` (TENANT_ID=287, USER_ID=1 상수 참조) +- **prices item_type_code 마이그레이션**: `api/database/migrations/2025_12_21_165524_update_prices_item_type_code_to_actual_item_type.php` +- **연관 문서**: `docs/plans/kd-orders-migration-plan.md` (입고/재고/주문 마이그레이션) + +--- + +## 10. 세션 및 메모리 관리 정책 + +### 10.1 세션 시작 시 (Load Strategy) +```bash +# 1. Docker 확인 +docker ps | grep sam + +# 2. DB 접속 테스트 +docker exec sam-mysql-1 mysql -uroot -proot chandj -e "SELECT COUNT(*) FROM KDunitprice;" +docker exec sam-mysql-1 mysql -uroot -proot samdb -e "SELECT COUNT(*) FROM items WHERE tenant_id=287;" + +# 3. 현재 진행 상태 확인 +# → 이 문서의 "📍 현재 진행 상태" 섹션 참조 + +# 4. 마이그레이션 상태 확인 (API 프로젝트) +cd /Users/kent/Works/@KD_SAM/SAM/api && php artisan migrate:status +``` + +### 10.2 작업 중 관리 + +| 작업 완료 시 | 조치 | +|-------------|------| +| Phase 완료 | "📍 현재 진행 상태" 업데이트 | +| INSERT 실행 | "12. 변경 이력" 추가 | +| 스키마 변경 | 관련 섹션 업데이트 + 변경 이력 추가 | +| 오류 발생 | 체크리스트에 메모 추가 | + +### 10.3 컨텍스트 관리 + +| 컨텍스트 잔량 | 조치 | +|--------------|------| +| **30% 이하** | 현재 작업 중단점 문서에 기록 | +| **20% 이하** | "📍 현재 진행 상태" 최종 업데이트 | +| **10% 이하** | 세션 정리 및 다음 세션 가이드 작성 | + +--- + +## 11. 자기완결성 점검 결과 + +### 11.1 체크리스트 검증 + +| # | 검증 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 1 | 작업 목적이 명확한가? | ✅ | 섹션 1.1 배경 | +| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 0 성공 기준 | +| 3 | 작업 범위가 구체적인가? | ✅ | 섹션 4 대상 범위 | +| 4 | 의존성이 명시되어 있는가? | ✅ | Quick Start 전제 조건 | +| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 9 참고 문서 | +| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 5 SQL 쿼리 | +| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 0 확인 방법 SQL | +| 8 | 모호한 표현이 없는가? | ✅ | 구체적 건수, 테이블명, 컬럼 명시 | + +### 11.2 새 세션 시뮬레이션 테스트 + +| 질문 | 답변 가능 | 참조 섹션 | +|------|:--------:|----------| +| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경, 문서 헤더 | +| Q2. 어디서부터 시작해야 하는가? | ✅ | 📍 현재 진행 상태 → 다음 작업 상세 | +| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 9. 참고 문서 | +| Q4. 작업 완료 확인 방법은? | ✅ | 0. 성공 기준 | +| Q5. 막혔을 때 참고 문서는? | ✅ | 9. 참고 문서, Quick Start DB 접속 명령어 | + +**결과**: 5/5 통과 → ✅ 자기완결성 확보 + +### 11.3 핵심 정보 요약 (새 세션용) + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 📋 핵심 정보 요약 │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 🎯 목표: 경동기업 레거시(chandj) → SAM(samdb) 품목/단가 이관 │ +│ │ +│ 📊 데이터 규모 (총 ~1,500건): │ +│ - items: ~800건 (KDunitprice 603 + 추가) │ +│ - prices: ~500건 │ +│ - item_bom_items: ~200건 │ +│ │ +│ 🔑 핵심 상수: │ +│ - tenant_id = 287 (경동기업) │ +│ - user_id = 1 (생성자) │ +│ - Docker: sam-mysql-1 │ +│ - 레거시 DB: chandj / SAM DB: samdb ⚠️ │ +│ │ +│ ⭐ KDunitprice 실제 컬럼명 (2026-01-28 확인): │ +│ - prodcode (품목코드) → items.code │ +│ - item_name (품목명) → items.name │ +│ - spec (규격) → items.attributes.spec │ +│ - unit (단위) → items.unit │ +│ - item_div ([제품] 등) → items.item_type │ +│ - unitprice (단가, 단일 컬럼!) → prices.sales_price │ +│ │ +│ ⭐ 마이그레이션 순서 (Seeder 기반): │ +│ 1. config/database.php에 'legacy' 연결 추가 │ +│ 2. .env에 LEGACY_DB_* 환경변수 추가 │ +│ 3. KyungdongItemSeeder.php 파일 생성 ← 최우선! │ +│ 4. Seeder 실행 (items 603건 + prices 603건) │ +│ 5. 추가 items/BOM은 확장 Seeder로 처리 │ +│ │ +│ 📍 현재 상태: Phase 1 대기 (Seeder 파일 생성 및 실행) │ +│ │ +│ ❌ 불필요한 것: item_id_mappings (양방향 조회 불필요, chandj 손 안댐) │ +│ │ +│ ⚠️ 주의: 모든 INSERT 실행 전 사용자 승인 필요 │ +│ │ +│ 📎 연관 문서: docs/plans/kd-orders-migration-plan.md (입고/재고/주문) │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 12. 변경 이력 + +| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | +|------|------|----------|------|------| +| 2026-01-28 | 문서 분리 | items-migration-kyungdong-plan.md에서 품목/단가 부분 분리 | - | - | +| 2026-01-28 | 문서 생성 | kd-items-migration-plan.md 신규 생성 | - | - | +| 2026-01-28 | 컬럼명 수정 | 실제 DB 컬럼명으로 업데이트 (품목코드→prodcode, 품목명→item_name 등) | - | - | +| 2026-01-28 | Seeder 전환 | SQL → Seeder 방식으로 전환, 섹션 5.0~5.6 구조 정리 | - | - | + +--- + +## 13. 트러블슈팅 가이드 + +### 13.1 일반적인 문제 + +| 문제 | 원인 | 해결책 | +|------|------|--------| +| Docker 컨테이너 없음 | Docker 미실행 | `docker-compose up -d` 실행 | +| DB 접속 실패 | 컨테이너명 변경 | `docker ps`로 정확한 컨테이너명 확인 | +| chandj DB 없음 | 레거시 DB 미설정 | Docker 볼륨 확인 또는 덤프 복원 | +| tenant_id 287 없음 | 경동기업 테넌트 미생성 | SAM에서 테넌트 생성 필요 | +| items 테이블 없음 | 마이그레이션 미실행 | `php artisan migrate` 실행 | +| **SAM DB 이름 오류** | `sam` 대신 `samdb` | 모든 쿼리에서 `samdb` 사용 확인 | +| KDunitprice 테이블 없음 | 레거시 덤프 불완전 | chandj 전체 덤프 확인 | + +### 13.2 JSON 파싱 오류 + +```php +// price_* 테이블의 itemList 파싱 시 주의사항 +$itemList = json_decode($record['itemList'], true); + +// 빈 값 또는 잘못된 JSON 처리 +if (empty($itemList) || !is_array($itemList)) { + // 스킵하고 로그 기록 + error_log("Invalid itemList in {$table} num={$record['num']}"); + continue; +} + +// 숫자 형식 변환 (콤마 제거) +$price = (float)str_replace(',', '', $item['col13'] ?? '0'); +``` + +### 13.3 중복 코드 처리 (code 기반) + +```sql +-- 이미 존재하는 품목 확인 (code 유일성 검사) +SELECT code, COUNT(*) AS cnt +FROM samdb.items +WHERE tenant_id=287 +GROUP BY code +HAVING cnt > 1; + +-- INSERT 시 ON DUPLICATE KEY UPDATE 사용 +-- ⚠️ items 테이블에 (tenant_id, code) UNIQUE 인덱스 필요 +INSERT INTO samdb.items (...) VALUES (...) +ON DUPLICATE KEY UPDATE name = VALUES(name), updated_at = NOW(); + +-- KDunitprice와 price_* 중복 확인 (⭐ 실제 컬럼명 사용) +SELECT k.prodcode, '모터 150K' AS price_item +FROM chandj.KDunitprice k +WHERE k.item_name LIKE '%모터%150K%'; +-- → KDunitprice가 마스터, price_*는 가격만 추가 +``` + +--- + +*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file