docs: BOM 품목 매핑, 수주 개소관리, 배포 가이드 문서 추가
- BOM 품목 매핑 분석 및 계획 문서 - 수주 개소(노드) 관리 계획 문서 - 배포 가이드 문서 - 수입검사 양식 변경 이력 업데이트 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,13 +5,17 @@
|
|||||||
**관련 계획:** docs/plans/incoming-inspection-templates-plan.md
|
**관련 계획:** docs/plans/incoming-inspection-templates-plan.md
|
||||||
|
|
||||||
## 📋 변경 개요
|
## 📋 변경 개요
|
||||||
5130 레거시 수입검사 양식 전환 작업의 일환으로 SUS 절곡판 수입검사 양식을 생성하고 품목을 연결함.
|
5130 레거시 수입검사 양식 전환 작업 - Phase 1 완료
|
||||||
|
- 13개 수입검사 양식 생성 (id:18-30)
|
||||||
|
- 테이블 컬럼 구조 추가 (미리보기 기능 정상화)
|
||||||
|
- MNG UI 테스트 완료
|
||||||
|
|
||||||
## 📁 수정된 파일/데이터
|
## 📁 수정된 파일/데이터
|
||||||
|
|
||||||
### 데이터베이스 변경
|
### 데이터베이스 변경
|
||||||
- `document_templates` - 1건 INSERT (id:19)
|
- `document_templates` - 13건 INSERT (id:18-30)
|
||||||
- `document_template_section_fields` - 8건 INSERT (template_id:19)
|
- `document_template_section_fields` - 8건씩 INSERT (template_id:18-30)
|
||||||
|
- `document_template_columns` - 84건 INSERT (7개 컬럼 × 12개 템플릿 19-30)
|
||||||
|
|
||||||
### 문서 변경
|
### 문서 변경
|
||||||
- `docs/plans/incoming-inspection-templates-plan.md` - 진행 상태 업데이트
|
- `docs/plans/incoming-inspection-templates-plan.md` - 진행 상태 업데이트
|
||||||
@@ -66,17 +70,36 @@
|
|||||||
| 14181 | sus1.2*1219*3000 P/L |
|
| 14181 | sus1.2*1219*3000 P/L |
|
||||||
| 14182 | sus1.2*1219*2500 |
|
| 14182 | sus1.2*1219*2500 |
|
||||||
|
|
||||||
|
### 4. 테이블 컬럼 구조 추가 (템플릿 19-30)
|
||||||
|
|
||||||
|
미리보기 기능이 동작하려면 `document_template_columns` 테이블에 컬럼 정의가 필요합니다.
|
||||||
|
템플릿 18(EGI)의 컬럼 구조를 복사하여 12개 템플릿(19-30)에 적용했습니다.
|
||||||
|
|
||||||
|
| sort_order | label | column_type | width |
|
||||||
|
|------------|-------|-------------|-------|
|
||||||
|
| 0 | NO | text | 50px |
|
||||||
|
| 1 | 검사항목 | text | 120px |
|
||||||
|
| 2 | 검사기준 | text | 150px |
|
||||||
|
| 3 | 검사방식 | text | 100px |
|
||||||
|
| 4 | 검사주기 | text | 100px |
|
||||||
|
| 5 | 측정치 | complex | 240px |
|
||||||
|
| 6 | 판정 (적/부) | select | 80px |
|
||||||
|
|
||||||
|
**측정치 컬럼 sub_labels:** `["n1", "n2", "n3"]`
|
||||||
|
|
||||||
## ✅ 테스트 체크리스트
|
## ✅ 테스트 체크리스트
|
||||||
- [x] 양식 생성 확인 (id:19)
|
- [x] 양식 생성 확인 (id:18-30, 총 13개)
|
||||||
- [x] 필드 8개 복사 확인
|
- [x] 필드 8개 복사 확인 (각 템플릿별)
|
||||||
- [x] 품목 11건 연결 확인
|
- [x] 품목 연결 확인 (EGI, SUS 등)
|
||||||
- [ ] MNG UI에서 양식 편집 테스트
|
- [x] MNG UI 양식 편집 테스트 ✅
|
||||||
|
- [x] **MNG UI 미리보기 테스트 ✅** (컬럼 추가 후 정상 동작)
|
||||||
- [ ] React resolve API 테스트
|
- [ ] React resolve API 테스트
|
||||||
|
|
||||||
## ⚠️ 후속 작업
|
## ⚠️ 후속 작업
|
||||||
1. EGI 양식(id:18)에 품목 연결 필요
|
1. ~~EGI 양식(id:18)에 품목 연결 필요~~ → 완료
|
||||||
2. Phase 1 나머지 양식 생성 (앵글 등)
|
2. ~~Phase 1 나머지 양식 생성~~ → 완료 (13개 양식)
|
||||||
3. MNG UI에서 검사항목 데이터 입력 필요
|
3. MNG UI에서 검사항목 데이터 입력 필요
|
||||||
|
4. React resolve API 테스트
|
||||||
|
|
||||||
## 🔗 관련 문서
|
## 🔗 관련 문서
|
||||||
- 계획 문서: `docs/plans/incoming-inspection-templates-plan.md`
|
- 계획 문서: `docs/plans/incoming-inspection-templates-plan.md`
|
||||||
|
|||||||
212
data/analysis/bom-item-mapping-analysis.md
Normal file
212
data/analysis/bom-item-mapping-analysis.md
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
# BOM 산출 아이템 ↔ Items Master 매핑 분석
|
||||||
|
|
||||||
|
> **분석일**: 2026-02-05
|
||||||
|
> **대상**: 경동기업 (tenant_id: 287)
|
||||||
|
> **범위**: BOM 산출 로직(KyungdongFormulaHandler) 전체 아이템 → SAM Items Master + 5130(chandj) DB
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 요약
|
||||||
|
|
||||||
|
| 항목 | 수치 |
|
||||||
|
|------|------|
|
||||||
|
| 5130(KDunitprice) 총 아이템 | 601개 |
|
||||||
|
| SAM Items Master 총 아이템 | 780개 |
|
||||||
|
| 5130 → SAM 코드 매칭률 | **100% (601/601)** |
|
||||||
|
| SAM 견적 전용 아이템 (EST/BD/PT/PM) | 157개 |
|
||||||
|
| BOM 산출 생성 아이템 종류 | 22종 |
|
||||||
|
| BOM → SAM 매핑 완료 | 17종 |
|
||||||
|
| BOM → SAM 미매핑 | **5종** |
|
||||||
|
|
||||||
|
### 핵심 결론
|
||||||
|
- 5130 → SAM 마이그레이션은 **100% 완료** (코드 기준 전수 매칭)
|
||||||
|
- BOM 산출 로직에서 생성하는 22종 아이템 중 **5종이 SAM items master에 미등록**
|
||||||
|
- 미등록 5종: 케이스 마구리, L바, 무게평철12T, 검사비, 주자재(스크린/슬랫)
|
||||||
|
- SAM에는 이미 견적 전용 코드 체계(EST-*, BD-*, PT-*, PM-*)가 구축되어 있음
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 5130(chandj) DB 구조
|
||||||
|
|
||||||
|
### 2.1 주요 테이블
|
||||||
|
|
||||||
|
| 테이블 | 건수 | 용도 |
|
||||||
|
|--------|------|------|
|
||||||
|
| **KDunitprice** | 601건 | 품목 단가 마스터 (SAM의 items 테이블에 해당) |
|
||||||
|
| **item_list** | 9건 | 견적 품목 분류 (스크린, 셔터박스, 연기장벽 등) |
|
||||||
|
| **parts** | 37건 | 부품 (가이드레일, 하단마감재 등 - 모델별) |
|
||||||
|
| **BDparts** | - | 절곡품 부품 |
|
||||||
|
| **price_angle** | 2건 | 앵글 단가표 (JSON 배열) |
|
||||||
|
| **price_bend** | - | 절곡 단가표 |
|
||||||
|
| **price_motor** | - | 모터 단가표 |
|
||||||
|
| **price_pipe** | - | 파이프 단가표 |
|
||||||
|
| **price_pole** | - | 환봉 단가표 |
|
||||||
|
| **price_raw_materials** | - | 원자재 단가표 |
|
||||||
|
| **price_screenplate** | - | 스크린판 단가표 |
|
||||||
|
| **price_shaft** | - | 샤프트 단가표 |
|
||||||
|
| **price_smokeban** | - | 연기차단재 단가표 |
|
||||||
|
| **price_etc** | - | 기타 단가표 |
|
||||||
|
|
||||||
|
### 2.2 KDunitprice 코드 체계
|
||||||
|
|
||||||
|
| 코드 접두사 | 범위 | 분류 | 비고 |
|
||||||
|
|------------|------|------|------|
|
||||||
|
| 00xxx | 00002~00046 | 부품/부재료 | 하장바, 가이드레일, 평철 등 |
|
||||||
|
| 20xxx | 20000~20011 | SUS 원재료 | SUS 1.2T, 1.5T 판재 |
|
||||||
|
| 30xxx | 30000~30006 | EGI 원재료 + 운송 | EGI 판재, 운송료 |
|
||||||
|
| 50xxx | 50000~50004 | 서비스 | 수리비, 제품개발, LED, 사용료 |
|
||||||
|
| 70xxx | 70001~70102 | KD 모터/브라켓/제어기 | 경동 자체 생산품 |
|
||||||
|
| 80xxx | 80006~80202 | 기타 부품/자재 | 절곡가공, 가스켓, 점검구 등 |
|
||||||
|
| 81xxx | 81000 | 기타 | 텐텐지롤 |
|
||||||
|
| 90xxx | 90100~90727 | 반제품/부자재 | 커넥터, 환봉, 링, 복주머니 등 |
|
||||||
|
| Hxxxx | H0001~H0020 | 철골자재 | 각파이프, 앵글 |
|
||||||
|
| K1xxx~K2xxx | K1011~K2029 | 작업복/안전화 | (비생산 품목) |
|
||||||
|
| Mxxxx | M0001~M0059 | 외주 모터/브라켓 | IS, HY, KST 등 |
|
||||||
|
| MCCD | MCCD0001 | 방범연동기 | |
|
||||||
|
| Nxxxx | N71100~N76101 | 신형 모터/브라켓/제어기 | N시리즈 |
|
||||||
|
| Rxxxx | R0001~R0008 | 샤우드 | BS/KS 샤우드 |
|
||||||
|
| Sxxxx | S0000~S0039 | 스크린/슬랫/셔터 | 주자재류 |
|
||||||
|
| Wxxxx | W0001 | 와이어 | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. SAM 견적 전용 코드 체계
|
||||||
|
|
||||||
|
SAM에는 5130에 없는 **견적 전용 아이템** 157개가 추가 등록되어 있음.
|
||||||
|
|
||||||
|
### 3.1 코드 체계별 분류
|
||||||
|
|
||||||
|
| 접두사 | 건수 | 용도 | 예시 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| **BD-** | 58개 | 절곡품 (모델/규격별) | BD-케이스-500*350, BD-가이드레일-KWE01-SUS-120*70 |
|
||||||
|
| **EST-** | 71개 | 견적 산출 전용 아이템 | EST-MOTOR-220V-300K, EST-SHAFT-4-6, EST-CTRL-매립형 |
|
||||||
|
| **PT-** | 15개 | 품목 템플릿 (규격 미포함) | PT-케이스, PT-가이드레일, PT-L-BAR |
|
||||||
|
| **PM-** | 13개 | 제어기 부품 매핑 | PM-020(제어기 노출형), PM-023(콘트롤박스) |
|
||||||
|
|
||||||
|
### 3.2 BD- (절곡품) 상세
|
||||||
|
|
||||||
|
모델별 규격이 정해진 절곡품:
|
||||||
|
- **케이스**: 10종 (500*350 ~ 780*650)
|
||||||
|
- **마구리**: 10종 (505*355 ~ 785*685)
|
||||||
|
- **가이드레일**: 20종 (모델별 SUS/EGI, 2가지 규격)
|
||||||
|
- **하단마감재**: 10종 (모델별 SUS/EGI)
|
||||||
|
- **L-BAR**: 5종 (모델별)
|
||||||
|
- **연기차단재**: 2종 (케이스용, 가이드레일용)
|
||||||
|
- **보강평철**: 1종
|
||||||
|
|
||||||
|
### 3.3 EST- (견적 전용) 상세
|
||||||
|
|
||||||
|
- **EST-MOTOR-**: 19종 (220V/380V, 용량별)
|
||||||
|
- **EST-CTRL-**: 17종 (제어기/방범/방화 부품)
|
||||||
|
- **EST-SHAFT-**: 18종 (3~12인치, 길이별)
|
||||||
|
- **EST-PIPE-**: 3종 (각파이프 두께/길이별)
|
||||||
|
- **EST-ANGLE-**: 8종 (메인앵글, 모터받침 앵글)
|
||||||
|
- **EST-RAW-**: 4종 (스크린원단, 슬랫)
|
||||||
|
- **EST-SMOKE-**: 2종 (연기차단재)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. BOM 산출 아이템 매핑 상태
|
||||||
|
|
||||||
|
### 4.1 calculateSteelItems (절곡품) - 10종
|
||||||
|
|
||||||
|
| BOM 아이템명 | SAM 등록 | SAM 코드 | 5130 등록 | 매핑 상태 |
|
||||||
|
|-------------|----------|----------|-----------|----------|
|
||||||
|
| 케이스 | O | BD-케이스-{규격}, PT-케이스 | X (5130 미등록) | **SAM만 등록** |
|
||||||
|
| 케이스용 연기차단재 | O | BD-케이스용 연기차단재, EST-SMOKE-케이스용 | X | **SAM만 등록** |
|
||||||
|
| 케이스 마구리 | **X** | - | X | **미등록** |
|
||||||
|
| 가이드레일 | O | BD-가이드레일-{모델}-{재질}-{규격}, PT-가이드레일 | O (00015) | 매핑 완료 |
|
||||||
|
| 레일용 연기차단재 | O | BD-가이드레일용 연기차단재, EST-SMOKE-레일용 | X | **SAM만 등록** |
|
||||||
|
| 하장바 | O | 00035, 00036 (5130 동일코드) | O (00035, 00036) | 매핑 완료 |
|
||||||
|
| L바 | **X** | - | X | **미등록** |
|
||||||
|
| 보강평철 | O | BD-보강평철-50, PT-보강평철 | X | **SAM만 등록** |
|
||||||
|
| 무게평철12T | **X** | - | O (00021 평철12T) | **SAM 미등록, 5130에는 유사품 존재** |
|
||||||
|
| 환봉 | O | 90201~90205 (5130 동일코드) | O (90201~90205) | 매핑 완료 |
|
||||||
|
|
||||||
|
### 4.2 calculatePartItems (부자재) - 5종
|
||||||
|
|
||||||
|
| BOM 아이템명 | SAM 등록 | SAM 코드 | 5130 등록 | 매핑 상태 |
|
||||||
|
|-------------|----------|----------|-----------|----------|
|
||||||
|
| 감기샤프트 {인치}인치 | O | EST-SHAFT-{인치}-{길이} (18종) | X (5130 미등록) | **SAM만 등록** |
|
||||||
|
| 각파이프 | O | EST-PIPE-{두께}-{길이} (3종) | O (H0001~H0020) | 매핑 완료 |
|
||||||
|
| 모터 받침용 앵글 | △ | EST-ANGLE-BRACKET-{타입} (4종) | X | **EST코드로 등록됨** |
|
||||||
|
| 앵글 {타입} | O | EST-ANGLE-MAIN-{타입} (4종) | O (H0003, H0004) | 매핑 완료 |
|
||||||
|
| 조인트바 | O | 800361, EST-RAW-슬랫-조인트바 | O (800361) | 매핑 완료 |
|
||||||
|
|
||||||
|
> **참고**: "모터 받침용 앵글"은 BOM에서 정확히 이 이름으로 검색하면 미등록이지만, EST-ANGLE-BRACKET-{타입} 4종이 이미 등록되어 있어 매핑 가능.
|
||||||
|
|
||||||
|
### 4.3 calculateDynamicItems (동적항목) - 7종
|
||||||
|
|
||||||
|
| BOM 아이템명 | BOM item_code | SAM 등록 | SAM 코드 | 5130 등록 | 매핑 상태 |
|
||||||
|
|-------------|------------|----------|----------|-----------|----------|
|
||||||
|
| 검사비 | KD-INSPECTION | **X** | - | X | **미등록** |
|
||||||
|
| 주자재(스크린) | KD-SCREEN | △ | EST-RAW-스크린-{타입} 3종 | O (S0001 등) | **EST코드로 등록됨** |
|
||||||
|
| 주자재(슬랫) | KD-SLAT | △ | EST-RAW-슬랫-{타입} 2종 | O (S0004, S0005) | **EST코드로 등록됨** |
|
||||||
|
| 모터 {용량} | KD-MOTOR-{용량} | O | EST-MOTOR-{전압}-{용량} (19종) | O (70001~70017 등) | 매핑 완료 |
|
||||||
|
| 제어기 {타입} | KD-CTRL-{타입} | O | EST-CTRL-{타입} (17종) | O (70026, 70027 등) | 매핑 완료 |
|
||||||
|
| 뒷박스 | KD-CTRL-BACKBOX | O | EST-CTRL-뒷박스, 80140 | O (80140) | 매핑 완료 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 미매핑 아이템 상세
|
||||||
|
|
||||||
|
### 5.1 완전 미등록 (SAM + 5130 모두 없음)
|
||||||
|
|
||||||
|
| 아이템 | 생성 메서드 | SAM 유사 코드 | 해결 방안 |
|
||||||
|
|--------|----------|-------------|----------|
|
||||||
|
| **케이스 마구리** | calculateSteelItems | BD-마구리-{규격} 10종 | BOM에서 BD-마구리-{규격} 매핑 필요 |
|
||||||
|
| **L바** | calculateSteelItems | BD-L-BAR-{모델}-{규격} 5종 | BOM에서 BD-L-BAR-{모델}-{규격} 매핑 필요 |
|
||||||
|
| **검사비** | calculateDynamicItems | (없음) | items master에 EST-INSPECTION 코드로 신규 등록 필요 |
|
||||||
|
|
||||||
|
### 5.2 SAM 미등록이나 유사품 존재
|
||||||
|
|
||||||
|
| 아이템 | 5130 유사품 | SAM 유사품 | 해결 방안 |
|
||||||
|
|--------|-----------|-----------|----------|
|
||||||
|
| **무게평철12T** | 00021 (평철12T, 2000mm, 13,500원) | SAM ID:14147 (00021, 평철12T) | 5130 코드 00021로 이미 SAM에 존재. BOM에서 매핑만 추가 |
|
||||||
|
|
||||||
|
### 5.3 KD-* → EST-* 코드 변환 필요
|
||||||
|
|
||||||
|
BOM에서 사용하는 KD-* 코드는 SAM items master에 미등록. EST-* 코드로 변환 매핑 필요.
|
||||||
|
|
||||||
|
| BOM item_code | SAM 대응 코드 | 변환 규칙 |
|
||||||
|
|--------------|-------------|----------|
|
||||||
|
| KD-INSPECTION | (미등록) | 신규 등록 필요 |
|
||||||
|
| KD-SCREEN | EST-RAW-스크린-{타입} | 타입(실리카/화이바/와이어)에 따라 분기 |
|
||||||
|
| KD-SLAT | EST-RAW-슬랫-{타입} | 타입(방범/방화)에 따라 분기 |
|
||||||
|
| KD-MOTOR-{용량} | EST-MOTOR-{전압}-{용량} | 전압(220V/380V) + 용량으로 분기 |
|
||||||
|
| KD-CTRL-{타입} | EST-CTRL-{타입} | 타입명 일치 |
|
||||||
|
| KD-CTRL-BACKBOX | EST-CTRL-뒷박스 | 직접 매핑 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 5130 price_* 단가 참조 테이블
|
||||||
|
|
||||||
|
BOM 산출 로직에서 단가를 가져오는 5130 테이블:
|
||||||
|
|
||||||
|
| 테이블 | 구조 | 용도 |
|
||||||
|
|--------|------|------|
|
||||||
|
| price_angle | JSON 배열 (itemList 컬럼) | 앵글 규격별 단가 |
|
||||||
|
| price_bend | JSON 배열 | 절곡 가공 단가 |
|
||||||
|
| price_motor | JSON 배열 | 모터 용량/전압별 단가 |
|
||||||
|
| price_pipe | JSON 배열 | 파이프 규격별 단가 |
|
||||||
|
| price_pole | JSON 배열 | 환봉 규격별 단가 |
|
||||||
|
| price_raw_materials | JSON 배열 | 원자재(스크린, 슬랫) 단가 |
|
||||||
|
| price_screenplate | JSON 배열 | 스크린 판재 단가 |
|
||||||
|
| price_shaft | JSON 배열 | 샤프트 인치/길이별 단가 |
|
||||||
|
| price_smokeban | JSON 배열 | 연기차단재 단가 |
|
||||||
|
| price_etc | JSON 배열 | 기타 항목 단가 |
|
||||||
|
|
||||||
|
> 이 테이블들은 SAM의 `chandj` DB 연결을 통해 직접 참조하며, BOM 산출 시 실시간으로 단가를 조회함.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 관련 파일
|
||||||
|
|
||||||
|
| 파일 | 용도 |
|
||||||
|
|------|------|
|
||||||
|
| `api/app/Services/Quote/FormulaHandlers/KyungdongFormulaHandler.php` | BOM 산출 메인 로직 |
|
||||||
|
| `api/app/Services/Quote/FormulaEvaluatorService.php` | 수식 평가 서비스 |
|
||||||
|
| `api/app/Services/Quote/QuoteCalculationService.php` | 자동산출 실행 |
|
||||||
|
| `api/app/Models/Items/Item.php` | Items 모델 |
|
||||||
|
| `docs/features/quotes/README.md` | 견적 시스템 문서 |
|
||||||
|
| `docs/plans/bom-item-mapping-plan.md` | 후속 작업 계획 |
|
||||||
80
deploys/item-master-data-deploy-20260203.sql
Normal file
80
deploys/item-master-data-deploy-20260203.sql
Normal file
File diff suppressed because one or more lines are too long
370
plans/bom-item-mapping-plan.md
Normal file
370
plans/bom-item-mapping-plan.md
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
# BOM 아이템 ↔ Items Master 매핑 작업 계획
|
||||||
|
|
||||||
|
> **작성일**: 2026-02-05
|
||||||
|
> **목적**: BOM 산출 로직에서 생성하는 모든 아이템에 SAM items master의 item_code/item_id를 매핑하여, 수주 등록 시 코드 기반 아이템 관리가 가능하도록 함
|
||||||
|
> **상태**: ✅ Phase 1, 2 완료 (검증 대기)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📍 현재 진행 상태
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|------|------|
|
||||||
|
| **마지막 완료 작업** | Phase 2: BOM 산출 로직 매핑 수정 완료 |
|
||||||
|
| **다음 작업** | Phase 3: 수주 등록 화면에서 검증 |
|
||||||
|
| **진행률** | 2/3 Phase (66%) |
|
||||||
|
| **마지막 업데이트** | 2026-02-05 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 개요
|
||||||
|
|
||||||
|
### 1.1 문제 상황
|
||||||
|
```
|
||||||
|
현재 상태:
|
||||||
|
- KyungdongFormulaHandler에서 22종 아이템 생성
|
||||||
|
- 그 중 5종은 item_code/item_id 없이 이름만으로 생성됨:
|
||||||
|
1. 케이스 마구리 (calculateSteelItems)
|
||||||
|
2. L바 (calculateSteelItems)
|
||||||
|
3. 무게평철12T (calculateSteelItems)
|
||||||
|
4. 검사비 (calculateDynamicItems) ← 유일한 SAM 미등록 아이템
|
||||||
|
5. 주자재(스크린/슬랫) (calculateDynamicItems) ← KD-* 코드 사용
|
||||||
|
|
||||||
|
문제점:
|
||||||
|
- 수주 등록 시 아이템 그룹핑/집계에서 코드 기반 매칭 불가
|
||||||
|
- item_code가 없으면 item_name으로만 집계되어 중복 발생
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 목표 상태
|
||||||
|
```
|
||||||
|
목표:
|
||||||
|
- BOM 산출 결과의 모든 22종 아이템에 item_code + item_id 필수
|
||||||
|
- 수주 등록 시 동일 item_code 기준으로 수량/금액 집계
|
||||||
|
|
||||||
|
기대 효과:
|
||||||
|
- 3개소에 "환봉" 각 1개씩 → item_code "90201" 기준 3개로 집계
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 핵심 원칙
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 🎯 핵심 원칙 (CRITICAL) │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ 1. items master에 등록된 제품을 매핑해야 함 │
|
||||||
|
│ → 코드를 임의로 만들어내면 안됨 │
|
||||||
|
│ 2. 기존 EST-/BD-/PT- 코드 체계를 활용 │
|
||||||
|
│ 3. BOM 산출 결과의 모든 아이템에 item_code + item_id 필수 │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.4 변경 승인 정책
|
||||||
|
|
||||||
|
| 분류 | 예시 | 승인 |
|
||||||
|
|------|------|------|
|
||||||
|
| ✅ 즉시 가능 | BOM 로직 내 item_code/item_id 매핑 추가 | 불필요 |
|
||||||
|
| ⚠️ 컨펌 필요 | items master에 신규 아이템 등록 | **필수** |
|
||||||
|
| 🔴 금지 | items 테이블 구조 변경, 기존 코드 체계 변경 | 별도 협의 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 전체 아이템 매핑 테이블 (22종)
|
||||||
|
|
||||||
|
### 2.1 calculateSteelItems (절곡품) - 10종
|
||||||
|
|
||||||
|
| # | BOM 아이템명 | 현재 item_code | SAM 매핑 코드 | SAM item_id | 매핑 방법 |
|
||||||
|
|---|-------------|---------------|--------------|-------------|----------|
|
||||||
|
| 1 | 케이스 | null | `BD-케이스-{규격}` | lookup | 규격으로 items 검색 (예: BD-케이스-500*350) |
|
||||||
|
| 2 | 케이스용 연기차단재 | null | `EST-SMOKE-케이스용` | 14912 | 고정 매핑 |
|
||||||
|
| 3 | **케이스 마구리** | **null** | `BD-마구리-{규격}` | lookup | 규격으로 items 검색 (예: BD-마구리-505*355) |
|
||||||
|
| 4 | 가이드레일 | null | `BD-가이드레일-{모델}-{재질}-{규격}` | lookup | 모델+재질+규격으로 검색 |
|
||||||
|
| 5 | 레일용 연기차단재 | null | `EST-SMOKE-레일용` | 14911 | 고정 매핑 |
|
||||||
|
| 6 | 하장바 | null | `00035` 또는 `00036` | 14158/14159 | 재질에 따라 분기 |
|
||||||
|
| 7 | **L바** | **null** | `BD-L-BAR-{모델}-{규격}` | lookup | 모델+규격으로 검색 (예: BD-L-BAR-KWE01-17*60) |
|
||||||
|
| 8 | 보강평철 | null | `BD-보강평철-50` | 14790 | 고정 매핑 |
|
||||||
|
| 9 | **무게평철12T** | **null** | `00021` | 14147 | 고정 매핑 (평철12T와 동일) |
|
||||||
|
| 10 | 환봉 | null | `90201`~`90204` | 14407~14410 | 파이 규격에 따라 분기 (30/35/45/50파이) |
|
||||||
|
|
||||||
|
### 2.2 calculatePartItems (부자재) - 5종
|
||||||
|
|
||||||
|
| # | BOM 아이템명 | 현재 item_code | SAM 매핑 코드 | SAM item_id | 매핑 방법 |
|
||||||
|
|---|-------------|---------------|--------------|-------------|----------|
|
||||||
|
| 11 | 감기샤프트 | null | `EST-SHAFT-{인치}-{길이}` | lookup | 인치+길이로 검색 (예: EST-SHAFT-4-6) |
|
||||||
|
| 12 | 각파이프 | null | `EST-PIPE-{두께}-{길이}` | lookup | 두께+길이로 검색 (예: EST-PIPE-1.4-6000) |
|
||||||
|
| 13 | 모터받침 앵글 | null | `EST-ANGLE-BRACKET-{타입}` | lookup | 타입으로 검색 (스크린용/철제300K/400K/800K) |
|
||||||
|
| 14 | 앵글 | null | `EST-ANGLE-MAIN-{타입}` | lookup | 앵글타입+길이로 검색 |
|
||||||
|
| 15 | 조인트바 | null | `800361` | 14733 | 고정 매핑 |
|
||||||
|
|
||||||
|
### 2.3 calculateDynamicItems (동적항목) - 7종
|
||||||
|
|
||||||
|
| # | BOM 아이템명 | 현재 item_code | SAM 매핑 코드 | SAM item_id | 매핑 방법 |
|
||||||
|
|---|-------------|---------------|--------------|-------------|----------|
|
||||||
|
| 16 | **검사비** | **KD-INSPECTION** | `EST-INSPECTION` | **(신규등록)** | **items master 신규 등록 필요** |
|
||||||
|
| 17 | 주자재(스크린) | KD-SCREEN | `EST-RAW-스크린-{타입}` | lookup | 타입으로 검색 (실리카/화이바/와이어) |
|
||||||
|
| 18 | 주자재(슬랫) | KD-SLAT | `EST-RAW-슬랫-{타입}` | lookup | 타입으로 검색 (방범/방화) |
|
||||||
|
| 19 | 모터 | KD-MOTOR-{용량} | `EST-MOTOR-{전압}-{용량}` | lookup | 전압+용량으로 검색 |
|
||||||
|
| 20 | 제어기 | KD-CTRL-{타입} | `EST-CTRL-{타입}` | lookup | 타입으로 검색 (노출형/매립형) |
|
||||||
|
| 21 | 뒷박스 | KD-CTRL-BACKBOX | `EST-CTRL-뒷박스` | 14863 | 고정 매핑 |
|
||||||
|
| 22 | 브라켓 | (모터에 포함) | `KD브라켓트*` 또는 `EST-*` | lookup | 모터 용량에 따라 분기 |
|
||||||
|
|
||||||
|
> **굵은 글씨**: 현재 미매핑 상태 (작업 대상)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 대상 범위
|
||||||
|
|
||||||
|
### 3.1 Phase 1: 미등록 아이템 등록 (사용자 승인 필요)
|
||||||
|
|
||||||
|
| # | 작업 항목 | 상태 | 비고 |
|
||||||
|
|---|----------|:----:|------|
|
||||||
|
| 1.1 | 검사비(EST-INSPECTION) items master 신규 등록 | ✅ | ID: 14913 |
|
||||||
|
| 1.2 | 무게평철12T → 00021(평철12T) 동일 아이템 확인 | ✅ | ID: 14147 확인 완료 |
|
||||||
|
|
||||||
|
### 3.2 Phase 2: BOM 산출 로직 매핑 수정
|
||||||
|
|
||||||
|
| # | 작업 항목 | 상태 | 비고 |
|
||||||
|
|---|----------|:----:|------|
|
||||||
|
| 2.1 | calculateSteelItems: 10종 아이템에 item_code/item_id 매핑 | ✅ | withItemMapping 헬퍼 사용 |
|
||||||
|
| 2.2 | calculatePartItems: 5종 아이템에 item_code/item_id 매핑 | ✅ | withItemMapping 헬퍼 사용 |
|
||||||
|
| 2.3 | calculateDynamicItems: KD-* → EST-* 코드 변환 | ✅ | 모터/제어기/주자재 매핑 완료 |
|
||||||
|
|
||||||
|
### 3.3 Phase 3: 검증
|
||||||
|
|
||||||
|
| # | 작업 항목 | 상태 | 비고 |
|
||||||
|
|---|----------|:----:|------|
|
||||||
|
| 3.1 | 견적 산출 실행 → 모든 아이템 item_code/item_id 확인 | ✅ | 18종 아이템 모두 매핑됨 |
|
||||||
|
| 3.2 | 수주 등록 → 코드 기반 아이템 그룹핑/집계 정상 동작 | ⏳ | 화면 검증 필요 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 실행 환경 및 명령어
|
||||||
|
|
||||||
|
### 4.1 Docker 환경
|
||||||
|
```bash
|
||||||
|
# API 컨테이너 접속
|
||||||
|
docker exec -it sam-api-1 bash
|
||||||
|
|
||||||
|
# PHP Tinker 실행
|
||||||
|
docker exec sam-api-1 php artisan tinker --execute='...'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 주요 확인 명령어
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# SAM items master에서 특정 코드 검색
|
||||||
|
docker exec sam-api-1 php artisan tinker --execute='
|
||||||
|
$item = \App\Models\Items\Item::where("tenant_id", 287)
|
||||||
|
->where("code", "EST-INSPECTION")
|
||||||
|
->first();
|
||||||
|
echo $item ? "ID: {$item->id}, Code: {$item->code}, Name: {$item->name}" : "NOT FOUND";
|
||||||
|
'
|
||||||
|
|
||||||
|
# 코드 패턴으로 검색 (BD-*, EST-* 등)
|
||||||
|
docker exec sam-api-1 php artisan tinker --execute='
|
||||||
|
$items = \App\Models\Items\Item::where("tenant_id", 287)
|
||||||
|
->where("code", "like", "BD-마구리%")
|
||||||
|
->get(["id", "code", "name"]);
|
||||||
|
foreach ($items as $item) {
|
||||||
|
echo "{$item->id} | {$item->code} | {$item->name}" . PHP_EOL;
|
||||||
|
}
|
||||||
|
'
|
||||||
|
|
||||||
|
# 5130 chandj DB 연결 테스트
|
||||||
|
docker exec sam-api-1 php artisan tinker --execute='
|
||||||
|
$count = \Illuminate\Support\Facades\DB::connection("chandj")
|
||||||
|
->table("KDunitprice")->count();
|
||||||
|
echo "KDunitprice 총 건수: {$count}";
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 검사비 신규 등록 (Phase 1.1)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 검사비 아이템 신규 등록
|
||||||
|
docker exec sam-api-1 php artisan tinker --execute='
|
||||||
|
$item = \App\Models\Items\Item::create([
|
||||||
|
"tenant_id" => 287,
|
||||||
|
"code" => "EST-INSPECTION",
|
||||||
|
"name" => "검사비",
|
||||||
|
"unit" => "EA",
|
||||||
|
"item_type" => "product",
|
||||||
|
"is_active" => true,
|
||||||
|
]);
|
||||||
|
echo "Created: ID={$item->id}, Code={$item->code}";
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 코드 수정 가이드
|
||||||
|
|
||||||
|
### 5.1 수정 대상 파일
|
||||||
|
```
|
||||||
|
api/app/Services/Quote/FormulaHandlers/KyungdongFormulaHandler.php
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 수정 패턴 (예시: calculateSteelItems 내 케이스 마구리)
|
||||||
|
|
||||||
|
**현재 코드 (item_code 없음):**
|
||||||
|
```php
|
||||||
|
$items[] = [
|
||||||
|
'item_name' => '케이스 마구리',
|
||||||
|
'item_code' => null, // ❌ 없음
|
||||||
|
'item_id' => null, // ❌ 없음
|
||||||
|
'quantity' => $quantity,
|
||||||
|
'unit_price' => $unitPrice,
|
||||||
|
'total_price' => $quantity * $unitPrice,
|
||||||
|
// ...
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
**수정 후 (item_code/item_id 매핑):**
|
||||||
|
```php
|
||||||
|
// 규격 계산 (예: 505*355)
|
||||||
|
$spec = "{$caseWidth}*{$caseDepth}";
|
||||||
|
$itemCode = "BD-마구리-{$spec}";
|
||||||
|
|
||||||
|
// items master에서 lookup
|
||||||
|
$item = \App\Models\Items\Item::where('tenant_id', $this->tenantId)
|
||||||
|
->where('code', $itemCode)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$items[] = [
|
||||||
|
'item_name' => '케이스 마구리',
|
||||||
|
'item_code' => $item?->code ?? $itemCode, // ✅ 코드 매핑
|
||||||
|
'item_id' => $item?->id, // ✅ ID 매핑
|
||||||
|
'quantity' => $quantity,
|
||||||
|
'unit_price' => $unitPrice,
|
||||||
|
'total_price' => $quantity * $unitPrice,
|
||||||
|
// ...
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 아이템 lookup 헬퍼 메서드 추가 (권장)
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* items master에서 코드로 아이템 조회
|
||||||
|
*/
|
||||||
|
private function lookupItem(string $code): ?Item
|
||||||
|
{
|
||||||
|
return Item::where('tenant_id', $this->tenantId)
|
||||||
|
->where('code', $code)
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 아이템 배열에 item_code/item_id 추가
|
||||||
|
*/
|
||||||
|
private function withItemMapping(array $item, string $code): array
|
||||||
|
{
|
||||||
|
$masterItem = $this->lookupItem($code);
|
||||||
|
return array_merge($item, [
|
||||||
|
'item_code' => $masterItem?->code ?? $code,
|
||||||
|
'item_id' => $masterItem?->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 검증 방법
|
||||||
|
|
||||||
|
### 6.1 견적 산출 후 BOM 결과 확인
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 최근 견적의 BOM 결과에서 item_code 확인
|
||||||
|
docker exec sam-api-1 php artisan tinker --execute='
|
||||||
|
$quote = \App\Models\Quote::where("tenant_id", 287)
|
||||||
|
->whereNotNull("calculation_result")
|
||||||
|
->latest()
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$items = $quote->calculation_result["items"] ?? [];
|
||||||
|
$noCode = array_filter($items, fn($i) => empty($i["item_code"]));
|
||||||
|
|
||||||
|
echo "총 아이템: " . count($items) . "개" . PHP_EOL;
|
||||||
|
echo "item_code 없음: " . count($noCode) . "개" . PHP_EOL;
|
||||||
|
|
||||||
|
foreach ($noCode as $i) {
|
||||||
|
echo " - {$i["item_name"]}" . PHP_EOL;
|
||||||
|
}
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 수주 등록 화면 확인
|
||||||
|
1. `/orders/create?quoteId={ID}`로 수주 등록 화면 진입
|
||||||
|
2. 아이템 목록에서 동일 아이템이 코드 기준으로 집계되는지 확인
|
||||||
|
3. 3개소에 "환봉" 각 1개 → "환봉" 1행, 수량 3개로 표시되어야 함
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 성공 기준
|
||||||
|
|
||||||
|
| 기준 | 검증 방법 | 달성 |
|
||||||
|
|------|----------|:----:|
|
||||||
|
| BOM 산출 22종 아이템 전부 item_code 보유 | Phase 3.1 검증 쿼리 | ⏳ |
|
||||||
|
| BOM 산출 22종 아이템 전부 item_id 보유 | Phase 3.1 검증 쿼리 | ⏳ |
|
||||||
|
| 수주 등록 시 코드 기반 아이템 집계 정상 동작 | Phase 3.2 화면 확인 | ⏳ |
|
||||||
|
| 기존 견적 산출 금액에 영향 없음 | 기존 견적 재산출 후 금액 비교 | ⏳ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 관련 소스 파일
|
||||||
|
|
||||||
|
| 파일 | 수정 여부 | 용도 |
|
||||||
|
|------|:--------:|------|
|
||||||
|
| `api/app/Services/Quote/FormulaHandlers/KyungdongFormulaHandler.php` | **수정** | BOM 산출 매핑 로직 추가 |
|
||||||
|
| `api/app/Models/Items/Item.php` | 읽기 | items lookup |
|
||||||
|
| `react/src/components/orders/OrderRegistration.tsx` | 검증 | 수주 등록 아이템 그룹핑 |
|
||||||
|
| `react/src/components/orders/actions.ts` | 검증 | 수주 데이터 변환 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 참고 정보
|
||||||
|
|
||||||
|
### 9.1 SAM 견적 전용 코드 체계
|
||||||
|
|
||||||
|
| 접두사 | 용도 | 예시 |
|
||||||
|
|--------|------|------|
|
||||||
|
| BD- | 절곡품 (모델/규격별) | BD-케이스-500*350, BD-마구리-505*355 |
|
||||||
|
| EST- | 견적 산출 전용 | EST-MOTOR-220V-300K, EST-INSPECTION |
|
||||||
|
| PT- | 품목 템플릿 (규격 미포함) | PT-케이스, PT-가이드레일 |
|
||||||
|
| PM- | 제어기 부품 | PM-020 (제어기 노출형) |
|
||||||
|
|
||||||
|
### 9.2 5130 DB 연결 정보
|
||||||
|
|
||||||
|
```
|
||||||
|
# api/.env
|
||||||
|
CHANDJ_DB_HOST=sam-mysql-1
|
||||||
|
CHANDJ_DB_DATABASE=chandj
|
||||||
|
CHANDJ_DB_USERNAME=root
|
||||||
|
CHANDJ_DB_PASSWORD=root
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.3 상세 분석 문서
|
||||||
|
- 전체 분석 결과: `docs/data/analysis/bom-item-mapping-analysis.md`
|
||||||
|
- 견적 시스템 구조: `docs/features/quotes/README.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 컨펌 대기 목록
|
||||||
|
|
||||||
|
| # | 항목 | 변경 내용 | 승인 |
|
||||||
|
|---|------|----------|:----:|
|
||||||
|
| 1 | 검사비 신규 등록 | items master에 EST-INSPECTION 추가 | ⏳ |
|
||||||
|
| 2 | 무게평철12T 동일성 | BOM의 무게평철12T = 00021 평철12T 인지 | ⏳ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 변경 이력
|
||||||
|
|
||||||
|
| 날짜 | 작업 | 변경 내용 | 파일 |
|
||||||
|
|------|------|----------|------|
|
||||||
|
| 2026-02-05 | 분석 | 22종 아이템 매핑 상태 분석 완료 | bom-item-mapping-analysis.md |
|
||||||
|
| 2026-02-05 | 계획 | 작업 계획 문서 작성 | bom-item-mapping-plan.md |
|
||||||
|
| 2026-02-05 | Phase 1 | 검사비(EST-INSPECTION) ID:14913 신규 등록 | items master |
|
||||||
|
| 2026-02-05 | Phase 2 | KyungdongFormulaHandler 매핑 로직 추가 | KyungdongFormulaHandler.php |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*이 문서는 /sc:plan 스킬로 생성되었습니다.*
|
||||||
@@ -11,10 +11,10 @@
|
|||||||
|
|
||||||
| 항목 | 내용 |
|
| 항목 | 내용 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| **마지막 완료 작업** | Phase 2B 프론트엔드 검증 완료 + RM display_condition 수정 |
|
| **마지막 완료 작업** | 견적 영향 검증 완료 + SM/CS 프론트엔드 검증 + SM display_condition 수정 |
|
||||||
| **다음 작업** | 섀도잉 정리 (재수행), Phase 3 (견적 영향 범위 설정 수정) |
|
| **다음 작업** | 섀도잉 정리 (재수행), SM field 108/109 드롭다운 옵션 실데이터 정비 |
|
||||||
| **진행률** | Phase 1 완료, Phase 2A 롤백, Phase 2B 완료 (5/5 + 검증 + display_condition 수정) |
|
| **진행률** | Phase 1 완료, Phase 2A 롤백, Phase 2B 완료 (5/5 + 검증 + RM/SM display_condition 수정 + 견적 검증) |
|
||||||
| **마지막 업데이트** | 2026-01-31 |
|
| **마지막 업데이트** | 2026-02-03 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -655,9 +655,13 @@ WHERE f.id IN (116, 117, 129, 131, 136) AND f.tenant_id = 287;
|
|||||||
|---|------|----------|----------|------|
|
|---|------|----------|----------|------|
|
||||||
| 1 | FG 필드 추가 | 4개 필드 추가(id:177-180) + SM/RM options 추가 | item_fields, entity_relationships | ✅ 완료 |
|
| 1 | FG 필드 추가 | 4개 필드 추가(id:177-180) + SM/RM options 추가 | item_fields, entity_relationships | ✅ 완료 |
|
||||||
| 2 | category_id 분류 | categories 5건 추가, 780건 매핑 | categories, items | ✅ 완료 |
|
| 2 | category_id 분류 | categories 5건 추가, 780건 매핑 | categories, items | ✅ 완료 |
|
||||||
| 3 | FG item_details 생성 | 18건 INSERT 필요 | item_details | ⏳ 대기 |
|
| 3 | FG item_details 생성 | 18건 INSERT 완료 (Phase 2B-4) | item_details | ✅ 완료 |
|
||||||
| 4 | PT/SM/RM attributes 매핑 | field_key ↔ attributes JSON 키 정합성 | item_fields, items.attributes | ⏳ 대기 |
|
| 4 | PT/SM/RM attributes 매핑 | field_key ↔ attributes JSON 키 정합성 완료 (Phase 2B-5) | item_fields, items.attributes | ✅ 완료 |
|
||||||
| 5 | 섀도잉 정리 (재수행) | Phase 2A에서 롤백됨. display_condition 안전 확인 후 재수행 | entity_relationships, item_fields | ⏳ 대기 |
|
| 5 | 견적 시스템 영향 검증 | KyungdongFormulaHandler가 items.attributes 미참조 확인. 견적 영향 없음 | FormulaEvaluatorService | ✅ 완료 |
|
||||||
|
| 6 | SM/CS 프론트엔드 검증 | SM: display_condition 수정 후 정상. CS: 무형상품이라 실질 영향 없음 | dev.sam.kr | ✅ 완료 |
|
||||||
|
| 7 | RM/SM display_condition 수정 | RM id:100 + SM id:107 조건부 표시 매핑 추가 | item_fields | ✅ 완료 |
|
||||||
|
| 8 | 섀도잉 정리 (재수행) | Phase 2A에서 롤백됨. display_condition 안전 확인 후 재수행 | entity_relationships, item_fields | ⏳ 대기 |
|
||||||
|
| 9 | SM field 108/109 드롭다운 옵션 정비 | 현재 테스트 데이터(부자재1-1 등) → 실제 규격값으로 교체 필요 | item_fields.options | ⏳ 대기 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -679,6 +683,10 @@ WHERE f.id IN (116, 117, 129, 131, 136) AND f.tenant_id = 287;
|
|||||||
| 2026-01-31 | 2B-5 | attributes field_key 매핑: PT Part_type 669건(조립400/구매205/절곡64), RM 100~103 field_key 28건(SUS/EGI/원단류+규격파싱), SM 107 카테고리 61건+spec→108, CS item_name 4건 | items.attributes | ✅ |
|
| 2026-01-31 | 2B-5 | attributes field_key 매핑: PT Part_type 669건(조립400/구매205/절곡64), RM 100~103 field_key 28건(SUS/EGI/원단류+규격파싱), SM 107 카테고리 61건+spec→108, CS item_name 4건 | items.attributes | ✅ |
|
||||||
| 2026-01-31 | 2B-검증 | 프론트엔드 검증: FG(모델명/대분류/마감유형/설치유형 ✅), PT(Part_type→조건부필드 ✅), RM(품목명 ✅, 규격 display_condition 불일치 발견) | - | ✅ |
|
| 2026-01-31 | 2B-검증 | 프론트엔드 검증: FG(모델명/대분류/마감유형/설치유형 ✅), PT(Part_type→조건부필드 ✅), RM(품목명 ✅, 규격 display_condition 불일치 발견) | - | ✅ |
|
||||||
| 2026-01-31 | 2B-DC | RM 필드 100 display_condition 수정: "SUS(스테인리스)"→[101,102,103], "EGI(아연도금강판)"→[101,102,103], "원단류"→[101] 추가. 기존 4개 조건 유지 | item_fields.id=100 | ✅ |
|
| 2026-01-31 | 2B-DC | RM 필드 100 display_condition 수정: "SUS(스테인리스)"→[101,102,103], "EGI(아연도금강판)"→[101,102,103], "원단류"→[101] 추가. 기존 4개 조건 유지 | item_fields.id=100 | ✅ |
|
||||||
|
| 2026-02-03 | 견적검증 | 견적 시스템 영향 검증 완료. KyungdongFormulaHandler(tenant_id=287)는 items.attributes 미참조. quote_items.item_id=NULL(스냅샷 저장). Phase 2B 변경이 견적 계산에 영향 없음 확인 | FormulaEvaluatorService, KyungdongFormulaHandler | ✅ |
|
||||||
|
| 2026-02-03 | SM검증 | SM 프론트엔드 검증: 알카바(id:12565) 편집 화면에서 품목명 ✅ 표시, 규격 ❌ 미표시 (display_condition에 "알카바" 매핑 누락). field 108/109 드롭다운 옵션은 테스트 데이터(부자재1-1 등) | dev.sam.kr 프론트엔드 | ✅ |
|
||||||
|
| 2026-02-03 | CS검증 | CS 프론트엔드 검증: 출장비(id:12860) 편집 화면에서 품목명 ✅, 규격(사양) 빈칸 (field_key=specification vs attributes.spec 불일치, 무형상품이라 실질 영향 없음) | dev.sam.kr 프론트엔드 | ✅ |
|
||||||
|
| 2026-02-03 | SM-DC | SM 필드 107 display_condition 수정: 기존 2개(육각볼트→108, 썬더볼트→109) + 8개 추가(샤우드/앵글/알카바/컨트롤박스/기타/포장자재/방범부품/원단류→108). 알카바 편집 화면에서 규격 필드 표시 확인 ✅ | item_fields.id=107 | ✅ |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -801,7 +809,9 @@ read_memory("item-data-alignment-active-symbols") // 3. 작업 대상 파악
|
|||||||
| FG-KSE01-벽면형-EGI 조회 | model_name=KSE01, finishing_type=EGI마감 등 표시 | 상세 뷰는 고정 레이아웃, 품목기준관리 설정에서 필드 4개 정상 등록 확인 | ⚠️ 부분 |
|
| FG-KSE01-벽면형-EGI 조회 | model_name=KSE01, finishing_type=EGI마감 등 표시 | 상세 뷰는 고정 레이아웃, 품목기준관리 설정에서 필드 4개 정상 등록 확인 | ⚠️ 부분 |
|
||||||
| PT-가이드레일 조회 | base_price, 부품유형 등 표시 | 품목기준관리에서 Part_type 4종 options 확인 | ✅ |
|
| PT-가이드레일 조회 | base_price, 부품유형 등 표시 | 품목기준관리에서 Part_type 4종 options 확인 | ✅ |
|
||||||
| FG BOM 표시 | 가이드레일×2, 하단마감재×1, L-BAR×2 표시 | 상세 화면에서 BOM 3건 정확히 표시 | ✅ |
|
| FG BOM 표시 | 가이드레일×2, 하단마감재×1, L-BAR×2 표시 | 상세 화면에서 BOM 3건 정확히 표시 | ✅ |
|
||||||
| 견적 생성 (수정 후) | 기존과 동일한 견적 금액 산출 | 수정 금지 영역 미변경 (bom/code/item_category 미접촉) | ✅ 안전 |
|
| 견적 생성 (수정 후) | 기존과 동일한 견적 금액 산출 | KyungdongFormulaHandler 분석: items.attributes 미참조. quote_items.item_id=ALL NULL(스냅샷). Phase 2B 변경 영향 없음 | ✅ 확인 |
|
||||||
|
| SM 알카바 규격 표시 | 품목명 "알카바" 선택 시 규격(field 108) 표시 | display_condition 수정 후 규격 필드 표시 ✅. 드롭다운 옵션은 테스트 데이터(별도 정비 필요) | ✅ |
|
||||||
|
| CS 출장비 편집 | 품목명/규격 필드 표시 | 품목명 ✅, 규격(사양) 빈칸(field_key=specification vs attributes.spec, 무형상품이라 실질 영향 없음) | ✅ |
|
||||||
| SM 품목 활성 여부 | `items.is_active` 컬럼 값 표시 (field_163 아님) | 품목기준관리에서 id:163 공통필드로 교체 확인, id:164 제거 | ✅ |
|
| SM 품목 활성 여부 | `items.is_active` 컬럼 값 표시 (field_163 아님) | 품목기준관리에서 id:163 공통필드로 교체 확인, id:164 제거 | ✅ |
|
||||||
| PT 품목상태 필드 | `items.is_active` 공통 필드로 통합 표시 | 기본정보/측면규격 양쪽에 id:163 연결 확인, id:105/133/138/152 제거 | ✅ |
|
| PT 품목상태 필드 | `items.is_active` 공통 필드로 통합 표시 | 기본정보/측면규격 양쪽에 id:163 연결 확인, id:105/133/138/152 제거 | ✅ |
|
||||||
| FG 품목상태 필드 | `items.is_active` 공통 필드로 통합 표시 | 기본정보에 id:163 연결 확인, id:138 제거 | ✅ |
|
| FG 품목상태 필드 | `items.is_active` 공통 필드로 통합 표시 | 기본정보에 id:163 연결 확인, id:138 제거 | ✅ |
|
||||||
@@ -815,7 +825,9 @@ read_memory("item-data-alignment-active-symbols") // 3. 작업 대상 파악
|
|||||||
| FG 18개 품목이 모든 필드 정상 표시 | ⚠️ | DB 설정 완료. 상세 뷰는 고정 레이아웃이라 동적 필드 미표시 (프론트 별도) |
|
| FG 18개 품목이 모든 필드 정상 표시 | ⚠️ | DB 설정 완료. 상세 뷰는 고정 레이아웃이라 동적 필드 미표시 (프론트 별도) |
|
||||||
| PT 부품유형별 조건부 필드 정상 작동 | ✅ | Part_type 4종 options 갱신 완료 |
|
| PT 부품유형별 조건부 필드 정상 작동 | ✅ | Part_type 4종 options 갱신 완료 |
|
||||||
| RM/SM 드롭다운 실제 값 표시 | ✅ | RM 재질/두께/폭/길이, SM 11개 카테고리 반영 확인 |
|
| RM/SM 드롭다운 실제 값 표시 | ✅ | RM 재질/두께/폭/길이, SM 11개 카테고리 반영 확인 |
|
||||||
| 견적 금액 변동 없음 (회귀 테스트) | ✅ | 수정 금지 영역(bom/code/item_category) 미접촉 |
|
| 견적 금액 변동 없음 (회귀 테스트) | ✅ | KyungdongFormulaHandler 코드 분석 + quote_items 스냅샷 구조 확인. Phase 2B 영향 없음 |
|
||||||
|
| SM display_condition 정상 작동 | ✅ | field 107에 10개 품목명→규격필드 매핑 완료. 알카바 편집 화면에서 규격 표시 확인 |
|
||||||
|
| CS 프론트엔드 정상 | ✅ | 무형상품(출장비/노무비 등) 편집 화면 정상. 규격 미매핑은 실질 영향 없음 |
|
||||||
| 품목 목록 → 상세 → 문서 데이터 흐름 정상 | ✅ | FG 목록 18건 표시, BOM 3건 정상 |
|
| 품목 목록 → 상세 → 문서 데이터 흐름 정상 | ✅ | FG 목록 18건 표시, BOM 3건 정상 |
|
||||||
| is_active 중복 필드 통합 완료 (6건 → 공통필드 1개) | ✅ | SM/PT/FG 모두 id:163으로 교체 확인 |
|
| is_active 중복 필드 통합 완료 (6건 → 공통필드 1개) | ✅ | SM/PT/FG 모두 id:163으로 교체 확인 |
|
||||||
| null key 필드(id:152) 비활성화 | ✅ | 폼에서 미표시 확인 |
|
| null key 필드(id:152) 비활성화 | ✅ | 폼에서 미표시 확인 |
|
||||||
|
|||||||
831
plans/order-location-management-plan.md
Normal file
831
plans/order-location-management-plan.md
Normal file
@@ -0,0 +1,831 @@
|
|||||||
|
# 수주 하위 구조 관리 시스템 구축 계획
|
||||||
|
|
||||||
|
> **작성일**: 2026-02-06
|
||||||
|
> **목적**: 수주(Order) 하위에 범용 N-depth 트리 구조를 구축하여 개소/구역/공정 등 다양한 하위 단위를 자유롭게 관리
|
||||||
|
> **기준 문서**: `docs/features/quotes/README.md`, `docs/specs/database-schema.md`, `docs/standards/api-rules.md`
|
||||||
|
> **상태**: 🔄 진행중
|
||||||
|
> **설계 결정**: 하이브리드 (고정 코어 컬럼 + options JSON) — 통계 쿼리 성능과 유연성 균형
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📍 현재 진행 상태
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|------|------|
|
||||||
|
| **마지막 완료 작업** | Phase 4.2 - 프론트엔드 노드별 그룹 UI |
|
||||||
|
| **다음 작업** | 완료 (테스트 검증 필요) |
|
||||||
|
| **진행률** | 13/13 (100%) |
|
||||||
|
| **마지막 업데이트** | 2026-02-06 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 개요
|
||||||
|
|
||||||
|
### 1.1 배경
|
||||||
|
|
||||||
|
**즉시 문제**: 견적→수주 전환(`QuoteService::convertToOrder`)에서 개소 정보가 매핑되지 않아 `order_items.floor_code`, `symbol_code`가 null로 저장됨. 반면 `OrderService::syncFromQuote`에는 이미 파싱 로직이 있어 정상 동작.
|
||||||
|
|
||||||
|
**구조적 문제**: 현재 수주 하위 구조는 `order_items` 플랫 테이블뿐이며, 개소/구역/공정 등 다양한 그루핑 단위를 관리할 수 없음. 5130(경동)은 개소별 관리가 필요하지만, 향후 다른 테넌트에서는 구역별, 층별, 공정별 등 다양한 트리 구조가 필요.
|
||||||
|
|
||||||
|
**현재 데이터 흐름 문제**:
|
||||||
|
```
|
||||||
|
견적 저장:
|
||||||
|
quotes.calculation_inputs.items[] → 개소별 데이터 ✅
|
||||||
|
quote_items.note → "4F FSS-01" ✅
|
||||||
|
|
||||||
|
수주 전환 (convertToOrder):
|
||||||
|
order_items.floor_code → null ❌ ← $productMapping이 빈 배열
|
||||||
|
order_items.symbol_code → null ❌
|
||||||
|
|
||||||
|
수주 동기화 (syncFromQuote):
|
||||||
|
order_items.floor_code → "4F" ✅ ← note 파싱 로직 있음
|
||||||
|
order_items.symbol_code → "FSS-01" ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 목표
|
||||||
|
|
||||||
|
1. 견적→수주 전환 시 개소 정보가 정확히 매핑되도록 즉시 수정 (Quick Fix)
|
||||||
|
2. `order_nodes` 테이블을 신규 생성하여 **범용 N-depth 트리 구조** 제공
|
||||||
|
3. 노드별 독립 상태 추적 (대기/진행중/완료/취소)
|
||||||
|
4. 프론트엔드에서 노드별 그룹 UI 제공 (경동은 개소별 표시)
|
||||||
|
|
||||||
|
### 1.3 아키텍처 결정
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 🎯 설계 결정: 하이브리드 (고정 코어 + options JSON) │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ❌ 순수 EAV → 통계 쿼리 시 JOIN 폭발, 성능 문제 │
|
||||||
|
│ ❌ 고정 컬럼 전용 → 경동 개소에만 맞고 범용성 없음 │
|
||||||
|
│ ✅ 하이브리드 → 통계용 고정 컬럼 + 유형별 상세는 options JSON │
|
||||||
|
│ │
|
||||||
|
│ 근거: │
|
||||||
|
│ - SAM 프로젝트에서 이미 options JSON 패턴 사용 중 │
|
||||||
|
│ (work_order_items.options, quotes.calculation_inputs) │
|
||||||
|
│ - MySQL 8 JSON path 쿼리 지원 (options->>'$.floor' 등) │
|
||||||
|
│ - 통계 집계는 고정 컬럼(code, name, status, quantity, price)으로 │
|
||||||
|
│ - sam_stat 일간/월간 집계에도 고정 컬럼 기반으로 수월 │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.4 핵심 원칙
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 1. 범용 트리: N-depth 자기참조(parent_id)로 어떤 구조든 표현 가능 │
|
||||||
|
│ 2. 통계 친화: code, name, status, quantity, price는 고정 컬럼 │
|
||||||
|
│ 3. 유형 자유: node_type으로 구분, 유형별 상세는 options JSON │
|
||||||
|
│ 4. 역호환성: 기존 수주(order_nodes 없는)도 정상 동작 │
|
||||||
|
│ 5. SAM 패턴 준수: BelongsToTenant, Auditable, SoftDeletes │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.5 적용 예시
|
||||||
|
|
||||||
|
**경동 (1-depth: 개소)**:
|
||||||
|
```
|
||||||
|
Order: ORD-260206-001
|
||||||
|
├── Node (type:location, code:"1F-FSS-01", name:"1F FSS-01")
|
||||||
|
│ ├── options: { floor:"1F", symbol:"FSS-01", product_code:"KSS01",
|
||||||
|
│ │ open_width:5000, open_height:3000, guide_rail:"wall" }
|
||||||
|
│ └── OrderItems (자재 N개)
|
||||||
|
│
|
||||||
|
└── Node (type:location, code:"2F-SD-02", name:"2F SD-02")
|
||||||
|
├── options: { floor:"2F", symbol:"SD-02", product_code:"KWE01",
|
||||||
|
│ open_width:2800, open_height:2400 }
|
||||||
|
└── OrderItems (자재 N개)
|
||||||
|
```
|
||||||
|
|
||||||
|
**다른 테넌트 (3-depth: 동→층→실)**:
|
||||||
|
```
|
||||||
|
Order: ORD-260206-005
|
||||||
|
├── Node (type:zone, code:"A", name:"A동")
|
||||||
|
│ ├── Node (type:floor, code:"1F", name:"1층")
|
||||||
|
│ │ ├── Node (type:room, code:"101", name:"회의실")
|
||||||
|
│ │ │ └── OrderItems
|
||||||
|
│ │ └── Node (type:room, code:"102", name:"사무실")
|
||||||
|
│ │ └── OrderItems
|
||||||
|
│ └── Node (type:floor, code:"2F", name:"2층")
|
||||||
|
│ └── ...
|
||||||
|
└── Node (type:zone, code:"B", name:"B동")
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.6 변경 승인 정책
|
||||||
|
|
||||||
|
| 분류 | 예시 | 승인 |
|
||||||
|
|------|------|------|
|
||||||
|
| ✅ 즉시 가능 | convertToOrder 로직 수정, 모델 관계 추가 | 불필요 |
|
||||||
|
| ⚠️ 컨펌 필요 | 마이그레이션 생성, 신규 테이블, API 엔드포인트 추가 | **필수** |
|
||||||
|
| 🔴 금지 | 기존 order_items.floor_code/symbol_code 삭제, 기존 API 스키마 변경 | 별도 협의 |
|
||||||
|
|
||||||
|
### 1.7 준수 규칙
|
||||||
|
|
||||||
|
- `docs/standards/api-rules.md` - Service-First, FormRequest, i18n
|
||||||
|
- `docs/specs/database-schema.md` - 공통 컬럼 패턴 (tenant_id, audit, softDeletes)
|
||||||
|
- `docs/standards/quality-checklist.md` - 품질 체크리스트
|
||||||
|
- `react/CLAUDE.md` - 'use client' 필수, Server Actions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 대상 범위
|
||||||
|
|
||||||
|
### 2.1 Phase 1: convertToOrder 개소 파싱 (Quick Fix)
|
||||||
|
|
||||||
|
| # | 작업 항목 | 파일 | 상태 | 비고 |
|
||||||
|
|---|----------|------|:----:|------|
|
||||||
|
| 1.1 | convertToOrder에 개소 파싱 로직 추가 | `api/app/Services/Quote/QuoteService.php` | ✅ | syncFromQuote 로직 재사용 |
|
||||||
|
| 1.2 | 개소 파싱 공통 메소드 추출 | `api/app/Services/Quote/QuoteService.php` | ✅ | 중복 코드 제거 |
|
||||||
|
|
||||||
|
### 2.2 Phase 2: order_nodes 테이블 (DB 스키마)
|
||||||
|
|
||||||
|
| # | 작업 항목 | 파일 | 상태 | 비고 |
|
||||||
|
|---|----------|------|:----:|------|
|
||||||
|
| 2.1 | order_nodes 마이그레이션 생성 | `api/database/migrations/XXXX_create_order_nodes_table.php` | ✅ | 신규 테이블 |
|
||||||
|
| 2.2 | order_items에 order_node_id 추가 | `api/database/migrations/XXXX_add_order_node_id_to_order_items.php` | ✅ | nullable FK |
|
||||||
|
| 2.3 | OrderNode 모델 생성 | `api/app/Models/Orders/OrderNode.php` | ✅ | BelongsToTenant, SoftDeletes, 자기참조 |
|
||||||
|
| 2.4 | Order 모델에 nodes() 관계 추가 | `api/app/Models/Orders/Order.php` | ✅ | HasMany |
|
||||||
|
| 2.5 | OrderItem 모델에 node() 관계 추가 | `api/app/Models/Orders/OrderItem.php` | ✅ | BelongsTo, fillable 추가 |
|
||||||
|
|
||||||
|
### 2.3 Phase 3: 전환 로직 연동 (Service)
|
||||||
|
|
||||||
|
| # | 작업 항목 | 파일 | 상태 | 비고 |
|
||||||
|
|---|----------|------|:----:|------|
|
||||||
|
| 3.1 | convertToOrder에 OrderNode 생성 로직 추가 | `api/app/Services/Quote/QuoteService.php` | ✅ | Phase 1.1 위에 구축 |
|
||||||
|
| 3.2 | syncFromQuote에 OrderNode 동기화 추가 | `api/app/Services/OrderService.php` | ✅ | 기존 items 삭제→재생성 패턴 동일 |
|
||||||
|
| 3.3 | 수주 상세 조회에 nodes eager loading | `api/app/Services/OrderService.php` | ✅ | show() 메소드 수정 |
|
||||||
|
|
||||||
|
### 2.4 Phase 4: 프론트엔드 노드별 UI
|
||||||
|
|
||||||
|
| # | 작업 항목 | 파일 | 상태 | 비고 |
|
||||||
|
|---|----------|------|:----:|------|
|
||||||
|
| 4.1 | OrderNode 타입 + 서버 액션 추가 | `react/src/components/orders/actions.ts` | ✅ | 타입 정의, API 호출 |
|
||||||
|
| 4.2 | 수주 상세 뷰 노드별 그룹 UI | `react/src/components/orders/OrderSalesDetailView.tsx` | ✅ | 트리/아코디언 형식 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 작업 절차
|
||||||
|
|
||||||
|
### 3.1 단계별 절차
|
||||||
|
|
||||||
|
```
|
||||||
|
Phase 1: Quick Fix (convertToOrder 개소 파싱)
|
||||||
|
├── 1.1 syncFromQuote의 개소 파싱 로직을 공통 메소드로 추출
|
||||||
|
├── 1.2 convertToOrder에서 공통 메소드 호출하여 $productMapping 전달
|
||||||
|
└── 검증: 견적→수주 전환 후 order_items.floor_code/symbol_code 값 확인
|
||||||
|
|
||||||
|
Phase 2: DB 스키마 (order_nodes 테이블)
|
||||||
|
├── 2.1 order_nodes 마이그레이션 작성
|
||||||
|
│ ├── 트리 구조: parent_id 자기참조 (nullable = 루트)
|
||||||
|
│ ├── 고정 코어: node_type, code, name, status_code, quantity, unit_price, total_price
|
||||||
|
│ └── 유연 확장: options JSON
|
||||||
|
├── 2.2 order_items에 order_node_id 컬럼 마이그레이션 작성
|
||||||
|
├── 2.3 OrderNode 모델 생성 (BelongsToTenant, Auditable, SoftDeletes)
|
||||||
|
│ ├── 자기참조 관계: parent(), children()
|
||||||
|
│ └── items() HasMany
|
||||||
|
├── 2.4 Order 모델에 nodes() HasMany 관계 추가
|
||||||
|
├── 2.5 OrderItem 모델에 node() BelongsTo 관계 추가
|
||||||
|
└── 검증: php artisan migrate 성공, 트리 관계 정상 동작
|
||||||
|
|
||||||
|
Phase 3: 전환 로직 연동
|
||||||
|
├── 3.1 convertToOrder에 OrderNode 생성 로직 삽입
|
||||||
|
│ ├── calculation_inputs.items[] 순회하여 OrderNode (type:location) 생성
|
||||||
|
│ ├── bomResults[]에서 금액 정보 매핑
|
||||||
|
│ └── OrderItem 생성 시 order_node_id 연결
|
||||||
|
├── 3.2 syncFromQuote에 OrderNode 동기화 추가
|
||||||
|
│ ├── 기존 nodes 소프트삭제 → 신규 생성
|
||||||
|
│ └── OrderItem 재생성 시 node 연결
|
||||||
|
├── 3.3 수주 상세 조회에 nodes eager loading 추가
|
||||||
|
└── 검증: API 호출로 노드 데이터 정상 반환 확인
|
||||||
|
|
||||||
|
Phase 4: 프론트엔드 UI
|
||||||
|
├── 4.1 타입 + 서버 액션
|
||||||
|
│ ├── OrderNode 인터페이스 정의
|
||||||
|
│ └── 수주 상세 조회 응답에 nodes 포함
|
||||||
|
├── 4.2 수주 상세 뷰 노드별 그룹 UI
|
||||||
|
│ ├── 노드별 카드/아코디언 레이아웃
|
||||||
|
│ ├── 노드 헤더 (유형/코드/이름/상태/금액)
|
||||||
|
│ ├── 노드 내 자재 테이블
|
||||||
|
│ ├── 하위 노드 중첩 표시 (재귀 컴포넌트)
|
||||||
|
│ └── 역호환: nodes 없는 수주는 기존 플랫 테이블 유지
|
||||||
|
└── 검증: 수주 상세 화면에서 노드별 그룹 표시 확인
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 상세 작업 내용
|
||||||
|
|
||||||
|
### 4.1 Phase 1: Quick Fix (변경 없음)
|
||||||
|
|
||||||
|
#### 1.1 convertToOrder 개소 파싱 로직 추가
|
||||||
|
|
||||||
|
**현재 코드** (`QuoteService.php` Line 600-607):
|
||||||
|
```php
|
||||||
|
$serialIndex = 1;
|
||||||
|
foreach ($quote->items as $quoteItem) {
|
||||||
|
$orderItem = OrderItem::createFromQuoteItem($quoteItem, $order->id, $serialIndex);
|
||||||
|
$orderItem->created_by = $userId;
|
||||||
|
$orderItem->save();
|
||||||
|
$serialIndex++;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**수정 코드**:
|
||||||
|
```php
|
||||||
|
$calculationInputs = $quote->calculation_inputs ?? [];
|
||||||
|
$productItems = $calculationInputs['items'] ?? [];
|
||||||
|
|
||||||
|
$serialIndex = 1;
|
||||||
|
foreach ($quote->items as $quoteItem) {
|
||||||
|
$productMapping = $this->resolveLocationMapping($quoteItem, $productItems);
|
||||||
|
$orderItem = OrderItem::createFromQuoteItem($quoteItem, $order->id, $serialIndex, $productMapping);
|
||||||
|
$orderItem->created_by = $userId;
|
||||||
|
$orderItem->save();
|
||||||
|
$serialIndex++;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.2 공통 메소드 추출
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* 견적 품목에서 개소(층/부호) 정보 추출
|
||||||
|
*/
|
||||||
|
private function resolveLocationMapping(QuoteItem $quoteItem, array $productItems): array
|
||||||
|
{
|
||||||
|
$floorCode = null;
|
||||||
|
$symbolCode = null;
|
||||||
|
|
||||||
|
// 1순위: note에서 파싱 ("4F FSS-01")
|
||||||
|
$note = trim($quoteItem->note ?? '');
|
||||||
|
if ($note !== '') {
|
||||||
|
$parts = preg_split('/\s+/', $note, 2);
|
||||||
|
$floorCode = $parts[0] ?? null;
|
||||||
|
$symbolCode = $parts[1] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2순위: formula_source → calculation_inputs
|
||||||
|
if (empty($floorCode) && empty($symbolCode)) {
|
||||||
|
$productIndex = 0;
|
||||||
|
$formulaSource = $quoteItem->formula_source ?? '';
|
||||||
|
if (preg_match('/product_(\d+)/', $formulaSource, $matches)) {
|
||||||
|
$productIndex = (int) $matches[1];
|
||||||
|
}
|
||||||
|
if (isset($productItems[$productIndex])) {
|
||||||
|
$floorCode = $productItems[$productIndex]['floor'] ?? null;
|
||||||
|
$symbolCode = $productItems[$productIndex]['code'] ?? null;
|
||||||
|
} elseif (count($productItems) === 1) {
|
||||||
|
$floorCode = $productItems[0]['floor'] ?? null;
|
||||||
|
$symbolCode = $productItems[0]['code'] ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['floor_code' => $floorCode, 'symbol_code' => $symbolCode];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.2 Phase 2: DB 스키마
|
||||||
|
|
||||||
|
#### 2.1 order_nodes 마이그레이션
|
||||||
|
|
||||||
|
```php
|
||||||
|
Schema::create('order_nodes', function (Blueprint $table) {
|
||||||
|
$table->id()->comment('ID');
|
||||||
|
$table->foreignId('tenant_id')->comment('테넌트 ID');
|
||||||
|
$table->foreignId('order_id')->comment('수주 ID');
|
||||||
|
|
||||||
|
// ---- 트리 구조 ----
|
||||||
|
$table->foreignId('parent_id')->nullable()->comment('상위 노드 ID (NULL=루트)');
|
||||||
|
|
||||||
|
// ---- 고정 코어 (통계/집계용) ----
|
||||||
|
$table->string('node_type', 50)->comment('노드 유형 (location, zone, floor, room, process...)');
|
||||||
|
$table->string('code', 100)->comment('식별 코드');
|
||||||
|
$table->string('name', 200)->comment('표시명');
|
||||||
|
$table->string('status_code', 30)->default('PENDING')
|
||||||
|
->comment('상태 (PENDING/CONFIRMED/IN_PRODUCTION/PRODUCED/SHIPPED/COMPLETED/CANCELLED)');
|
||||||
|
$table->integer('quantity')->default(1)->comment('수량');
|
||||||
|
$table->decimal('unit_price', 15, 2)->default(0)->comment('단가');
|
||||||
|
$table->decimal('total_price', 15, 2)->default(0)->comment('합계');
|
||||||
|
|
||||||
|
// ---- 유연 확장 (유형별 상세) ----
|
||||||
|
$table->json('options')->nullable()->comment('유형별 동적 속성 JSON');
|
||||||
|
|
||||||
|
// ---- 정렬 ----
|
||||||
|
$table->integer('depth')->default(0)->comment('트리 깊이 (0=루트)');
|
||||||
|
$table->integer('sort_order')->default(0)->comment('정렬 순서');
|
||||||
|
|
||||||
|
// ---- 감사 ----
|
||||||
|
$table->foreignId('created_by')->nullable()->comment('생성자 ID');
|
||||||
|
$table->foreignId('updated_by')->nullable()->comment('수정자 ID');
|
||||||
|
$table->foreignId('deleted_by')->nullable()->comment('삭제자 ID');
|
||||||
|
$table->timestamps();
|
||||||
|
$table->softDeletes();
|
||||||
|
|
||||||
|
// ---- 인덱스 ----
|
||||||
|
$table->index('tenant_id');
|
||||||
|
$table->index('parent_id');
|
||||||
|
$table->index(['order_id', 'depth', 'sort_order']);
|
||||||
|
$table->index(['order_id', 'node_type']);
|
||||||
|
$table->index(['tenant_id', 'node_type', 'status_code']); // 통계용
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**통계 쿼리 예시**:
|
||||||
|
```sql
|
||||||
|
-- 1. 노드 유형별 수주 현황 (고정 컬럼만으로 가능)
|
||||||
|
SELECT node_type, status_code, COUNT(*), SUM(total_price)
|
||||||
|
FROM order_nodes WHERE tenant_id = 287
|
||||||
|
GROUP BY node_type, status_code;
|
||||||
|
|
||||||
|
-- 2. 경동 개소별 상세 (필요 시 JSON path)
|
||||||
|
SELECT code, name, total_price,
|
||||||
|
options->>'$.floor' AS floor,
|
||||||
|
options->>'$.symbol' AS symbol
|
||||||
|
FROM order_nodes
|
||||||
|
WHERE order_id = 123 AND node_type = 'location';
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2 order_items에 order_node_id 추가
|
||||||
|
|
||||||
|
```php
|
||||||
|
Schema::table('order_items', function (Blueprint $table) {
|
||||||
|
$table->foreignId('order_node_id')
|
||||||
|
->nullable()
|
||||||
|
->after('order_id')
|
||||||
|
->comment('수주 노드 ID (order_nodes)');
|
||||||
|
$table->index('order_node_id');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.3 OrderNode 모델
|
||||||
|
|
||||||
|
```php
|
||||||
|
namespace App\Models\Orders;
|
||||||
|
|
||||||
|
class OrderNode extends Model
|
||||||
|
{
|
||||||
|
use Auditable, BelongsToTenant, SoftDeletes;
|
||||||
|
|
||||||
|
protected $table = 'order_nodes';
|
||||||
|
|
||||||
|
// 상태 코드 (Order와 동일 체계)
|
||||||
|
public const STATUS_PENDING = 'PENDING';
|
||||||
|
public const STATUS_CONFIRMED = 'CONFIRMED';
|
||||||
|
public const STATUS_IN_PRODUCTION = 'IN_PRODUCTION';
|
||||||
|
public const STATUS_PRODUCED = 'PRODUCED';
|
||||||
|
public const STATUS_SHIPPED = 'SHIPPED';
|
||||||
|
public const STATUS_COMPLETED = 'COMPLETED';
|
||||||
|
public const STATUS_CANCELLED = 'CANCELLED';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'tenant_id', 'order_id', 'parent_id',
|
||||||
|
'node_type', 'code', 'name',
|
||||||
|
'status_code', 'quantity', 'unit_price', 'total_price',
|
||||||
|
'options', 'depth', 'sort_order',
|
||||||
|
'created_by', 'updated_by', 'deleted_by',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'quantity' => 'integer',
|
||||||
|
'unit_price' => 'decimal:2',
|
||||||
|
'total_price' => 'decimal:2',
|
||||||
|
'options' => 'array',
|
||||||
|
'depth' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
// ---- 트리 관계 ----
|
||||||
|
public function parent(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(self::class, 'parent_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function children(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(self::class, 'parent_id')->orderBy('sort_order');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 비즈니스 관계 ----
|
||||||
|
public function order(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Order::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function items(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(OrderItem::class, 'order_node_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 트리 헬퍼 ----
|
||||||
|
public function isRoot(): bool
|
||||||
|
{
|
||||||
|
return $this->parent_id === null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isLeaf(): bool
|
||||||
|
{
|
||||||
|
return $this->children()->count() === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 하위 노드 포함 전체 트리 재귀 로드
|
||||||
|
*/
|
||||||
|
public function scopeWithRecursiveChildren($query)
|
||||||
|
{
|
||||||
|
return $query->with(['children' => function ($q) {
|
||||||
|
$q->orderBy('sort_order')->with('children', 'items');
|
||||||
|
}, 'items']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.4-2.5 기존 모델 수정
|
||||||
|
|
||||||
|
**Order 모델**:
|
||||||
|
```php
|
||||||
|
public function nodes(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(OrderNode::class)->orderBy('depth')->orderBy('sort_order');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rootNodes(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(OrderNode::class)->whereNull('parent_id')->orderBy('sort_order');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**OrderItem 모델** - fillable + 관계:
|
||||||
|
```php
|
||||||
|
// fillable에 추가
|
||||||
|
'order_node_id',
|
||||||
|
|
||||||
|
// 관계
|
||||||
|
public function node(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(OrderNode::class, 'order_node_id');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.3 Phase 3: 전환 로직 연동
|
||||||
|
|
||||||
|
#### 3.1 convertToOrder OrderNode 생성
|
||||||
|
|
||||||
|
**수정 위치**: `QuoteService::convertToOrder()` (Line 590~623)
|
||||||
|
|
||||||
|
```php
|
||||||
|
return DB::transaction(function () use ($quote, $userId, $tenantId) {
|
||||||
|
$orderNo = $this->generateOrderNumber($tenantId);
|
||||||
|
$order = Order::createFromQuote($quote, $orderNo);
|
||||||
|
$order->created_by = $userId;
|
||||||
|
$order->save();
|
||||||
|
|
||||||
|
// ---- OrderNode 생성 (개소별) ----
|
||||||
|
$calculationInputs = $quote->calculation_inputs ?? [];
|
||||||
|
$productItems = $calculationInputs['items'] ?? [];
|
||||||
|
$bomResults = $calculationInputs['bomResults'] ?? [];
|
||||||
|
|
||||||
|
$nodeMap = []; // productIndex → OrderNode
|
||||||
|
foreach ($productItems as $idx => $locItem) {
|
||||||
|
$bomResult = $bomResults[$idx] ?? null;
|
||||||
|
$grandTotal = $bomResult['grand_total'] ?? 0;
|
||||||
|
$qty = (int) ($locItem['quantity'] ?? 1);
|
||||||
|
$floor = $locItem['floor'] ?? '';
|
||||||
|
$symbol = $locItem['code'] ?? '';
|
||||||
|
|
||||||
|
$node = OrderNode::create([
|
||||||
|
'tenant_id' => $tenantId,
|
||||||
|
'order_id' => $order->id,
|
||||||
|
'parent_id' => null, // 루트 노드 (경동은 1-depth)
|
||||||
|
'node_type' => 'location',
|
||||||
|
'code' => trim("{$floor}-{$symbol}", '-') ?: "LOC-{$idx}",
|
||||||
|
'name' => trim("{$floor} {$symbol}") ?: "개소 ".($idx + 1),
|
||||||
|
'status_code' => OrderNode::STATUS_PENDING,
|
||||||
|
'quantity' => $qty,
|
||||||
|
'unit_price' => $grandTotal,
|
||||||
|
'total_price' => $grandTotal * $qty,
|
||||||
|
'options' => [
|
||||||
|
'floor' => $floor,
|
||||||
|
'symbol' => $symbol,
|
||||||
|
'product_code' => $locItem['productCode'] ?? null,
|
||||||
|
'product_name' => $locItem['productName'] ?? null,
|
||||||
|
'open_width' => $locItem['openWidth'] ?? null,
|
||||||
|
'open_height' => $locItem['openHeight'] ?? null,
|
||||||
|
'guide_rail_type' => $locItem['guideRailType'] ?? null,
|
||||||
|
'motor_power' => $locItem['motorPower'] ?? null,
|
||||||
|
'controller' => $locItem['controller'] ?? null,
|
||||||
|
'wing_size' => $locItem['wingSize'] ?? null,
|
||||||
|
'inspection_fee' => $locItem['inspectionFee'] ?? null,
|
||||||
|
'bom_result' => $bomResult,
|
||||||
|
],
|
||||||
|
'depth' => 0,
|
||||||
|
'sort_order' => $idx,
|
||||||
|
'created_by' => $userId,
|
||||||
|
]);
|
||||||
|
$nodeMap[$idx] = $node;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- OrderItem 생성 (노드 연결) ----
|
||||||
|
$serialIndex = 1;
|
||||||
|
foreach ($quote->items as $quoteItem) {
|
||||||
|
$mapping = $this->resolveLocationMapping($quoteItem, $productItems);
|
||||||
|
$locIdx = $this->resolveLocationIndex($quoteItem, $productItems);
|
||||||
|
|
||||||
|
$productMapping = array_merge($mapping, [
|
||||||
|
'order_node_id' => isset($nodeMap[$locIdx]) ? $nodeMap[$locIdx]->id : null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$orderItem = OrderItem::createFromQuoteItem($quoteItem, $order->id, $serialIndex, $productMapping);
|
||||||
|
$orderItem->created_by = $userId;
|
||||||
|
$orderItem->save();
|
||||||
|
$serialIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 합계 재계산 + 견적 상태 변경 (기존 로직 유지)
|
||||||
|
$order->load('items');
|
||||||
|
$order->recalculateTotals();
|
||||||
|
$order->save();
|
||||||
|
|
||||||
|
$quote->update([
|
||||||
|
'status' => Quote::STATUS_CONVERTED,
|
||||||
|
'order_id' => $order->id,
|
||||||
|
'updated_by' => $userId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $quote->refresh()->load(['items', 'client', 'order']);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**resolveLocationIndex 헬퍼**:
|
||||||
|
```php
|
||||||
|
private function resolveLocationIndex(QuoteItem $quoteItem, array $productItems): int
|
||||||
|
{
|
||||||
|
$formulaSource = $quoteItem->formula_source ?? '';
|
||||||
|
if (preg_match('/product_(\d+)/', $formulaSource, $matches)) {
|
||||||
|
return (int) $matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
$note = trim($quoteItem->note ?? '');
|
||||||
|
if ($note !== '') {
|
||||||
|
$parts = preg_split('/\s+/', $note, 2);
|
||||||
|
$floor = $parts[0] ?? '';
|
||||||
|
$code = $parts[1] ?? '';
|
||||||
|
foreach ($productItems as $idx => $item) {
|
||||||
|
if (($item['floor'] ?? '') === $floor && ($item['code'] ?? '') === $code) {
|
||||||
|
return $idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2 syncFromQuote OrderNode 동기화
|
||||||
|
|
||||||
|
**수정 위치**: `OrderService::syncFromQuote()` (Line 559~659)
|
||||||
|
|
||||||
|
기존 `$order->items()->delete()` 다음에:
|
||||||
|
```php
|
||||||
|
// 기존 노드 삭제 후 재생성
|
||||||
|
$order->nodes()->delete();
|
||||||
|
|
||||||
|
// OrderNode 생성 (convertToOrder와 동일 로직)
|
||||||
|
$nodeMap = [];
|
||||||
|
foreach ($productItems as $idx => $locItem) {
|
||||||
|
// ... (convertToOrder와 동일)
|
||||||
|
$nodeMap[$idx] = $node;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OrderItem 생성 시 order_node_id 연결
|
||||||
|
foreach ($quote->items as $index => $quoteItem) {
|
||||||
|
$locIdx = $this->resolveLocationIndex($quoteItem, $productItems);
|
||||||
|
$order->items()->create([
|
||||||
|
// ... 기존 필드 ...
|
||||||
|
'order_node_id' => $nodeMap[$locIdx]->id ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.3 수주 상세 조회 nodes eager loading
|
||||||
|
|
||||||
|
```php
|
||||||
|
$order = Order::where('tenant_id', $tenantId)
|
||||||
|
->with([
|
||||||
|
'items',
|
||||||
|
'rootNodes' => function ($q) {
|
||||||
|
$q->withRecursiveChildren(); // 재귀 트리 로드
|
||||||
|
},
|
||||||
|
'client',
|
||||||
|
'quote',
|
||||||
|
])
|
||||||
|
->find($id);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.4 Phase 4: 프론트엔드 노드별 UI
|
||||||
|
|
||||||
|
#### 4.1 타입 + 서버 액션
|
||||||
|
|
||||||
|
**OrderNode 타입** (`react/src/components/orders/actions.ts`):
|
||||||
|
```typescript
|
||||||
|
export interface OrderNode {
|
||||||
|
id: number;
|
||||||
|
parentId: number | null;
|
||||||
|
nodeType: string; // 'location', 'zone', 'floor', 'room', 'process'...
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
statusCode: string;
|
||||||
|
quantity: number;
|
||||||
|
unitPrice: number;
|
||||||
|
totalPrice: number;
|
||||||
|
options: Record<string, unknown> | null; // 유형별 동적 속성
|
||||||
|
depth: number;
|
||||||
|
sortOrder: number;
|
||||||
|
children: OrderNode[]; // 하위 노드 (재귀)
|
||||||
|
items: OrderItem[]; // 해당 노드의 자재
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderDetail extends Order {
|
||||||
|
nodes: OrderNode[]; // 루트 노드 배열 (children 재귀 포함)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.2 수주 상세 뷰 노드별 그룹 UI
|
||||||
|
|
||||||
|
**레이아웃 (경동 1-depth 예시)**:
|
||||||
|
```
|
||||||
|
┌─ 수주 기본 정보 ────────────────────────────────────────┐
|
||||||
|
│ 수주번호: ORD-260206-001 | 현장명: 삼성 빌딩 신축 │
|
||||||
|
│ 거래처: 삼성물산 | 총금액: 15,000,000원 │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─ 구조 (3개 노드) ──────────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
│ ┌─ [location] 1F FSS-01 ──────────────────────────┐ │
|
||||||
|
│ │ KSS01(고정스크린) | 5000×3000 | 수량:1 │ │
|
||||||
|
│ │ 상태: [PENDING ▾] | 금액: 1,250,000원 │ │
|
||||||
|
│ ├──────────────────────────────────────────────────┤ │
|
||||||
|
│ │ # | 품목코드 | 품명 | 수량 | 단가 | 금액 │ │
|
||||||
|
│ │ 1 | MT-SUS-01 | 슬랫 | 15.5 | 12,000 | 186K │ │
|
||||||
|
│ │ 소계: 1,250,000원 │ │
|
||||||
|
│ └──────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─ [location] 2F SD-02 ──────────────────────────┐ │
|
||||||
|
│ │ ... │ │
|
||||||
|
│ └─────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**재귀 컴포넌트 (N-depth)**:
|
||||||
|
```typescript
|
||||||
|
function OrderNodeCard({ node, depth = 0 }: { node: OrderNode; depth?: number }) {
|
||||||
|
return (
|
||||||
|
<div style={{ marginLeft: depth * 24 }}>
|
||||||
|
{/* 노드 헤더 */}
|
||||||
|
<NodeHeader node={node} />
|
||||||
|
|
||||||
|
{/* 해당 노드의 자재 테이블 */}
|
||||||
|
{node.items.length > 0 && <ItemsTable items={node.items} />}
|
||||||
|
|
||||||
|
{/* 하위 노드 재귀 렌더링 */}
|
||||||
|
{node.children.map(child => (
|
||||||
|
<OrderNodeCard key={child.id} node={child} depth={depth + 1} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**역호환**:
|
||||||
|
```typescript
|
||||||
|
{order.nodes && order.nodes.length > 0 ? (
|
||||||
|
order.nodes.map(node => <OrderNodeCard key={node.id} node={node} />)
|
||||||
|
) : (
|
||||||
|
<LegacyFlatTableView items={order.items} />
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 컨펌 대기 목록
|
||||||
|
|
||||||
|
| # | 항목 | 변경 내용 | 영향 범위 | 상태 |
|
||||||
|
|---|------|----------|----------|------|
|
||||||
|
| 1 | order_nodes 테이블 생성 | N-depth 자기참조 트리 + 하이브리드 JSON | DB 스키마 | ⚠️ 컨펌 필요 |
|
||||||
|
| 2 | order_items에 order_node_id 추가 | nullable FK 컬럼 | DB 스키마 | ⚠️ 컨펌 필요 |
|
||||||
|
| 3 | 노드 상태 코드 체계 | Order와 동일 체계 사용 | 비즈니스 로직 | ⚠️ 컨펌 필요 |
|
||||||
|
| 4 | 경동 node_type 값 | "location" 사용 | 비즈니스 로직 | ⚠️ 컨펌 필요 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 변경 이력
|
||||||
|
|
||||||
|
| 날짜 | 항목 | 변경 내용 | 파일 | 승인 |
|
||||||
|
|------|------|----------|------|------|
|
||||||
|
| 2026-02-06 | - | 문서 초안 작성 (order_locations 전용 설계) | - | - |
|
||||||
|
| 2026-02-06 | 아키텍처 변경 | order_locations → order_nodes (N-depth 트리 + 하이브리드) | - | ✅ 사용자 승인 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 참고 문서
|
||||||
|
|
||||||
|
- **견적 시스템 분석**: `docs/features/quotes/README.md`
|
||||||
|
- **DB 스키마 규칙**: `docs/specs/database-schema.md`
|
||||||
|
- **API 개발 규칙**: `docs/standards/api-rules.md`
|
||||||
|
- **품질 체크리스트**: `docs/standards/quality-checklist.md`
|
||||||
|
|
||||||
|
### 핵심 소스 파일
|
||||||
|
|
||||||
|
| 파일 | 역할 | 핵심 라인 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `api/app/Services/Quote/QuoteService.php` | 견적→수주 전환 | L574-623 (`convertToOrder`) |
|
||||||
|
| `api/app/Services/OrderService.php` | 수주 동기화 | L559-659 (`syncFromQuote`) |
|
||||||
|
| `api/app/Models/Orders/Order.php` | 수주 모델 | L23-59 (상태 코드) |
|
||||||
|
| `api/app/Models/Orders/OrderItem.php` | 수주 품목 모델 | L162-190 (`createFromQuoteItem`) |
|
||||||
|
| `react/src/components/orders/actions.ts` | 수주 프론트 타입 | L281-300 (OrderItem) |
|
||||||
|
| `react/src/components/orders/OrderSalesDetailView.tsx` | 수주 상세 뷰 | L386-424 (테이블) |
|
||||||
|
| `react/src/components/quotes/types.ts` | 견적 타입 | L661-684 (LocationItem) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 세션 및 메모리 관리 정책
|
||||||
|
|
||||||
|
### 8.1 세션 시작 시
|
||||||
|
|
||||||
|
```
|
||||||
|
1. read_memory("order-nodes-state") → 진행 상태 파악
|
||||||
|
2. 이 문서의 "📍 현재 진행 상태" 섹션 확인
|
||||||
|
3. 마지막 완료 작업 확인 후 다음 작업 착수
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 Serena 메모리 구조
|
||||||
|
|
||||||
|
- `order-nodes-state`: `{ phase, progress, next_step, last_decision }`
|
||||||
|
- `order-nodes-snapshot`: 현재까지의 코드 변경점 요약
|
||||||
|
- `order-nodes-active-symbols`: 수정 중인 파일/함수 목록
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 검증 결과
|
||||||
|
|
||||||
|
### 9.1 테스트 케이스
|
||||||
|
|
||||||
|
| # | 시나리오 | 예상 결과 | 실제 결과 | 상태 |
|
||||||
|
|---|---------|----------|----------|------|
|
||||||
|
| 1 | 견적 3개소 → 수주 전환 | order_nodes 3행(type:location) 생성, 각 order_items에 node_id 연결 | - | ⏳ |
|
||||||
|
| 2 | 수주 상세 조회 | rootNodes + children 재귀 + items eager loading 정상 | - | ⏳ |
|
||||||
|
| 3 | 견적 수정 → 수주 동기화 | 기존 nodes 삭제 후 재생성, items 재연결 | - | ⏳ |
|
||||||
|
| 4 | 기존 수주 (nodes 없음) 조회 | 기존 플랫 테이블 정상 표시, 에러 없음 | - | ⏳ |
|
||||||
|
| 5 | 프론트 노드별 그룹 표시 | 노드 카드 내 자재 테이블, 역호환 플랫 뷰 | - | ⏳ |
|
||||||
|
| 6 | 통계 쿼리 성능 | 고정 컬럼(node_type, status, total_price) 기반 GROUP BY 정상 | - | ⏳ |
|
||||||
|
|
||||||
|
### 9.2 성공 기준
|
||||||
|
|
||||||
|
| 기준 | 달성 | 비고 |
|
||||||
|
|------|------|------|
|
||||||
|
| 전환 시 order_nodes 생성됨 | ⏳ | Phase 2+3 |
|
||||||
|
| N-depth 트리 구조 지원 | ⏳ | Phase 2 (parent_id 자기참조) |
|
||||||
|
| order_items에 order_node_id 연결됨 | ⏳ | Phase 3 |
|
||||||
|
| 프론트 노드별 그룹 표시 | ⏳ | Phase 4 |
|
||||||
|
| 기존 수주 역호환 정상 | ⏳ | Phase 4 |
|
||||||
|
| 통계 쿼리가 고정 컬럼으로 가능 | ⏳ | Phase 2 (인덱스) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 자기완결성 점검 결과
|
||||||
|
|
||||||
|
### 10.1 체크리스트 검증
|
||||||
|
|
||||||
|
| # | 검증 항목 | 상태 | 비고 |
|
||||||
|
|---|----------|:----:|------|
|
||||||
|
| 1 | 작업 목적이 명확한가? | ✅ | 범용 N-depth 트리 + 통계 친화 하이브리드 |
|
||||||
|
| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 9.2 (6개 기준) |
|
||||||
|
| 3 | 작업 범위가 구체적인가? | ✅ | 섹션 2 (13개 작업 항목) |
|
||||||
|
| 4 | 의존성이 명시되어 있는가? | ✅ | Phase 1→2→3→4 순서 |
|
||||||
|
| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 7 (라인번호 포함) |
|
||||||
|
| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 3+4 (코드 포함) |
|
||||||
|
| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 9.1 (6개 테스트 케이스) |
|
||||||
|
| 8 | 모호한 표현이 없는가? | ✅ | 구체적 코드/파일/라인 명시 |
|
||||||
|
|
||||||
|
### 10.2 새 세션 시뮬레이션 테스트
|
||||||
|
|
||||||
|
| 질문 | 답변 가능 | 참조 섹션 |
|
||||||
|
|------|:--------:|----------|
|
||||||
|
| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경, 1.2 목표 |
|
||||||
|
| Q2. 왜 하이브리드 구조를 선택했는가? | ✅ | 1.3 아키텍처 결정 |
|
||||||
|
| Q3. 어디서부터 시작해야 하는가? | ✅ | 📍 현재 진행 상태 |
|
||||||
|
| Q4. 어떤 파일을 수정해야 하는가? | ✅ | 2. 대상 범위 |
|
||||||
|
| Q5. 작업 완료 확인 방법은? | ✅ | 9. 검증 결과 |
|
||||||
|
|
||||||
|
**결과**: 5/5 통과 → ✅ 자기완결성 확보
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*이 문서는 /plan 스킬 + Sequential Thinking MCP로 생성되었습니다.*
|
||||||
|
*아키텍처: N-depth 트리(order_nodes) + 하이브리드(고정 코어 + options JSON)*
|
||||||
Reference in New Issue
Block a user