docs: [plans] BOM 트리 3단계 React 구현 요청서 재작성 (프론트엔드 전달용)
This commit is contained in:
375
plans/bom-tree-3level-react-request.md
Normal file
375
plans/bom-tree-3level-react-request.md
Normal file
@@ -0,0 +1,375 @@
|
||||
# BOM 트리 3단계 그룹 표시 — React 프론트엔드 구현 요청서
|
||||
|
||||
> **작성일**: 2026-03-18
|
||||
> **요청자**: R&D실 (백엔드 API 개발 완료)
|
||||
> **대상**: React 프론트엔드 개발자
|
||||
> **배포 상태**: 개발서버 배포 완료 (`api.dev.codebridge-x.com`)
|
||||
|
||||
---
|
||||
|
||||
## 1. 작업 개요
|
||||
|
||||
품목관리 > 제품상세 화면의 "부품 구성 (BOM)" 섹션을 **플랫 테이블**에서 **카테고리별 접힘/펼침 그룹 트리**로 교체한다.
|
||||
|
||||
**현재**: 17건의 부품이 번호순 테이블로 나열
|
||||
**변경**: 주자재/모터/제어기/절곡품/부자재/검사비 6개 그룹으로 묶어서 트리 표시
|
||||
|
||||
> API가 3단계 트리 구조를 이미 반환하므로, 프론트엔드는 렌더링만 변경하면 된다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 화면 설계
|
||||
|
||||
### 2.1 현재 화면 (교체 대상)
|
||||
|
||||
```
|
||||
부품 구성 (BOM) 총 17개 품목
|
||||
┌────┬──────────────────────┬──────────────────┬────────┬──────┐
|
||||
│ 번호 │ 품목코드 │ 품목명 │ 수량 │ 단위 │
|
||||
├────┼──────────────────────┼──────────────────┼────────┼──────┤
|
||||
│ 1 │ EST-RAW-슬랫-방화 │ 스크린 실리카 │ 10.65 │ m² │
|
||||
│ 2 │ EST-MOTOR-220V-150K │ 모터 150K(S) │ 1 │ EA │
|
||||
│ .. │ ... │ ... │ ... │ ... │
|
||||
│ 17 │ EST-INSPECTION │ 검사비 │ 1 │ EA │
|
||||
└────┴──────────────────────┴──────────────────┴────────┴──────┘
|
||||
```
|
||||
|
||||
### 2.2 변경 후 화면
|
||||
|
||||
```
|
||||
부품 구성 (BOM) 총 17개 품목 · 6개 그룹
|
||||
|
||||
▼ 주자재 (1건)
|
||||
│ PT EST-RAW-슬랫-방화 스크린 실리카 x10.65 m²
|
||||
|
||||
▼ 모터 (1건)
|
||||
│ PT EST-MOTOR-220V-150K 모터 150K(S) (220V) x1 EA
|
||||
|
||||
▼ 제어기 (1건)
|
||||
│ PT EST-CTRL-노출형 제어기 노출형 x1 EA
|
||||
|
||||
▼ 절곡품 (9건)
|
||||
│ PT BD-케이스-500*380 케이스 500*380 x3.22 m
|
||||
│ PT BD-케이스용-연기차단... 케이스용 연기차단재 x3.22 m
|
||||
│ PT BD-마구리-505*385 마구리 505*385 x1 EA
|
||||
│ PT BD-가이드레일-KSS02... 가이드레일 KSS02... x6.5 m
|
||||
│ PT BD-가이드레일용-연기... 가이드레일용 연기... x6.5 m
|
||||
│ PT 00035 철재용하장바(SUS)3000 x3 m
|
||||
│ PT BD-L-BAR-KSS02-17*60 L-BAR KSS02 17*60 x3.22 m
|
||||
│ PT BD-보강평철-50 보강평철 50 x3.22 m
|
||||
│ PT 90201 KD환봉(30파이) x1 EA
|
||||
|
||||
▶ 부자재 (4건) ← 접힌 상태 (클릭하면 펼침)
|
||||
|
||||
▼ 검사비 (1건)
|
||||
│ PT EST-INSPECTION 검사비 x1 Dev
|
||||
```
|
||||
|
||||
### 2.3 동작 설명
|
||||
|
||||
| 동작 | 설명 |
|
||||
|------|------|
|
||||
| 카테고리 헤더 클릭 | 하위 품목 접힘/펼침 토글 |
|
||||
| 기본 상태 | 모든 카테고리 펼침 |
|
||||
| 품목 행 클릭 | 기존과 동일 (해당 품목 상세 표시 등) |
|
||||
|
||||
---
|
||||
|
||||
## 3. API 엔드포인트
|
||||
|
||||
### 3.1 BOM 트리 조회 — `GET /api/v1/items/{id}/bom/tree`
|
||||
|
||||
기존 엔드포인트. 인증: `X-API-KEY` + `Bearer Token` (기존과 동일).
|
||||
|
||||
**응답 예시** (`GET /api/v1/items/15531/bom/tree`):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "조회 완료",
|
||||
"data": {
|
||||
"id": 15531,
|
||||
"code": "FG-KSS02-측면형-SUS",
|
||||
"name": "KSS02 스크린 SUS마감 측면형",
|
||||
"item_type": "FG",
|
||||
"unit": "EA",
|
||||
"depth": 1,
|
||||
"children": [
|
||||
{
|
||||
"id": 0,
|
||||
"code": "",
|
||||
"name": "주자재",
|
||||
"item_type": "CAT",
|
||||
"unit": "",
|
||||
"depth": 2,
|
||||
"count": 1,
|
||||
"children": [
|
||||
{
|
||||
"id": 15657,
|
||||
"code": "EST-RAW-슬랫-방화",
|
||||
"name": "스크린 실리카",
|
||||
"item_type": "PT",
|
||||
"unit": "EA",
|
||||
"depth": 3,
|
||||
"quantity": 10.65,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 0,
|
||||
"code": "",
|
||||
"name": "모터",
|
||||
"item_type": "CAT",
|
||||
"unit": "",
|
||||
"depth": 2,
|
||||
"count": 1,
|
||||
"children": [
|
||||
{
|
||||
"id": 15627,
|
||||
"code": "EST-MOTOR-220V-150K",
|
||||
"name": "모터 150K(S) (220V)",
|
||||
"item_type": "PT",
|
||||
"unit": "EA",
|
||||
"depth": 3,
|
||||
"quantity": 1,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 0,
|
||||
"code": "",
|
||||
"name": "절곡품",
|
||||
"item_type": "CAT",
|
||||
"unit": "",
|
||||
"depth": 2,
|
||||
"count": 9,
|
||||
"children": [
|
||||
{
|
||||
"id": 15578,
|
||||
"code": "BD-케이스-500*380",
|
||||
"name": "케이스 500*380",
|
||||
"item_type": "PT",
|
||||
"unit": "m",
|
||||
"depth": 3,
|
||||
"quantity": 3.22,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 0,
|
||||
"code": "",
|
||||
"name": "부자재",
|
||||
"item_type": "CAT",
|
||||
"count": 4,
|
||||
"children": ["...4개 PT 품목..."]
|
||||
},
|
||||
{
|
||||
"id": 0,
|
||||
"code": "",
|
||||
"name": "검사비",
|
||||
"item_type": "CAT",
|
||||
"count": 1,
|
||||
"children": ["...1개 PT 품목..."]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 응답 구조 설명
|
||||
|
||||
| 필드 | 타입 | 설명 |
|
||||
|------|------|------|
|
||||
| `item_type` | `"FG"` / `"CAT"` / `"PT"` | FG=루트 완제품, **CAT=카테고리 그룹 (가상 노드)**, PT=실제 부품 |
|
||||
| `id` | number | CAT 노드는 `0` (가상), 나머지는 실제 품목 ID |
|
||||
| `count` | number | CAT 노드에만 존재 — 하위 품목 건수 |
|
||||
| `quantity` | number | PT 노드에만 존재 — 소요 수량 |
|
||||
| `children` | array | 하위 노드 배열 (재귀) |
|
||||
| `code` | string | CAT 노드는 빈 문자열, PT 노드는 품목코드 |
|
||||
|
||||
### 3.3 노드 판별 로직
|
||||
|
||||
```
|
||||
item_type === "CAT" → 카테고리 헤더로 렌더링 (접힘/펼침, count 표시)
|
||||
item_type !== "CAT" → 품목 행으로 렌더링 (코드, 품목명, 수량, 단위)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 구현 가이드
|
||||
|
||||
### 4.1 데이터 로드
|
||||
|
||||
```typescript
|
||||
// FG 품목 상세 페이지에서 BOM 트리 로드
|
||||
const [bomTree, setBomTree] = useState<BomTreeNode | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (item?.id && item?.itemType === 'FG') {
|
||||
fetch(`/api/proxy/items/${item.id}/bom/tree`)
|
||||
.then(res => res.json())
|
||||
.then(data => setBomTree(data.data));
|
||||
}
|
||||
}, [item?.id]);
|
||||
```
|
||||
|
||||
### 4.2 타입 정의
|
||||
|
||||
```typescript
|
||||
interface BomTreeNode {
|
||||
id: number;
|
||||
code: string;
|
||||
name: string;
|
||||
item_type: 'FG' | 'CAT' | 'PT' | string;
|
||||
unit: string;
|
||||
depth: number;
|
||||
count?: number; // CAT 노드 — 하위 품목 건수
|
||||
quantity?: number; // PT 노드 — 소요 수량
|
||||
children: BomTreeNode[];
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 렌더링 컴포넌트
|
||||
|
||||
```tsx
|
||||
function BomTreeNode({ node }: { node: BomTreeNode }) {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
const isCategory = node.item_type === 'CAT';
|
||||
const hasChildren = node.children && node.children.length > 0;
|
||||
|
||||
return (
|
||||
<div className={isCategory ? '' : 'ml-6'}>
|
||||
{/* 노드 행 */}
|
||||
<div
|
||||
className={`flex items-center gap-2 py-1.5 px-3 rounded ${
|
||||
isCategory
|
||||
? 'bg-gray-50 hover:bg-gray-100 cursor-pointer'
|
||||
: 'hover:bg-gray-50'
|
||||
}`}
|
||||
onClick={() => hasChildren && setIsOpen(!isOpen)}
|
||||
>
|
||||
{/* 토글 아이콘 */}
|
||||
{hasChildren && (
|
||||
isOpen
|
||||
? <ChevronDown className="h-4 w-4 text-gray-400 shrink-0" />
|
||||
: <ChevronRight className="h-4 w-4 text-gray-400 shrink-0" />
|
||||
)}
|
||||
{!hasChildren && <span className="w-4" />}
|
||||
|
||||
{isCategory ? (
|
||||
/* 카테고리 헤더 */
|
||||
<>
|
||||
<span className="text-sm font-semibold text-gray-700">{node.name}</span>
|
||||
<Badge variant="outline" className="text-xs">{node.count}건</Badge>
|
||||
</>
|
||||
) : (
|
||||
/* 품목 행 */
|
||||
<>
|
||||
<Badge className="text-xs">{node.item_type}</Badge>
|
||||
<code className="text-xs text-gray-400">{node.code}</code>
|
||||
<span className="text-sm text-gray-700 flex-1 truncate">{node.name}</span>
|
||||
{node.quantity && (
|
||||
<span className="text-xs font-medium text-blue-600 ml-auto">
|
||||
x{node.quantity}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs text-gray-400">{node.unit}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 하위 노드 (접힘/펼침) */}
|
||||
{isOpen && hasChildren && (
|
||||
<div className={isCategory ? 'border-l-2 border-gray-200 ml-2' : ''}>
|
||||
{node.children.map((child, i) => (
|
||||
<BomTreeNode key={child.id || `cat-${i}`} node={child} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 Card 섹션 교체
|
||||
|
||||
기존 `ItemDetailClient.tsx` 557~611줄의 BOM Card를 아래로 교체:
|
||||
|
||||
```tsx
|
||||
{item.itemType === 'FG' && bomTree && bomTree.children?.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Package className="h-5 w-5" />
|
||||
부품 구성 (BOM)
|
||||
</CardTitle>
|
||||
<Badge variant="outline" className="bg-blue-50 text-blue-700">
|
||||
총 {bomTree.children.reduce((sum, cat) => sum + (cat.count || 0), 0)}개 품목
|
||||
· {bomTree.children.length}개 그룹
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{bomTree.children.map((catNode, i) => (
|
||||
<BomTreeNode key={i} node={catNode} />
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 수정 대상 파일
|
||||
|
||||
| 파일 | 변경 내용 |
|
||||
|------|----------|
|
||||
| `src/components/items/ItemDetailClient.tsx` | 557~611줄 BOM Card → 트리 컴포넌트로 교체 |
|
||||
| `src/types/item.ts` (선택) | `BomTreeNode` 타입 추가 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 테스트 방법
|
||||
|
||||
1. 개발서버(`dev.codebridge-x.com`) 접속
|
||||
2. 품목관리 > FG 품목 선택 (예: `KSS02 스크린 SUS마감 측면형`)
|
||||
3. BOM 섹션에서 카테고리 그룹 표시 확인
|
||||
4. 각 카테고리 헤더 클릭 → 접힘/펼침 동작 확인
|
||||
5. MNG(`admin.codebridge-x.com`) > 품목관리에서 동일 품목의 BOM 탭과 비교
|
||||
|
||||
### 테스트 데이터
|
||||
|
||||
tenant `프론트_테스트회사`의 FG 품목 18건에 BOM 데이터가 등록되어 있다.
|
||||
|
||||
| 품목코드 | 품목명 | BOM 건수 |
|
||||
|---------|--------|---------|
|
||||
| FG-KSS01-벽면형-SUS | KSS01 스크린 SUS마감 벽면형 | 17건 |
|
||||
| FG-KSS02-측면형-SUS | KSS02 스크린 SUS마감 측면형 | 17건 |
|
||||
| FG-KQTS01-벽면형-SUS | KQTS01 철재 SUS마감 벽면형 | 17건 |
|
||||
| ... (총 18개 FG) | | |
|
||||
|
||||
---
|
||||
|
||||
## 7. 체크리스트
|
||||
|
||||
- [ ] `GET /api/v1/items/{id}/bom/tree` 호출하여 3단계 트리 데이터 로드
|
||||
- [ ] `item_type === "CAT"` 노드를 카테고리 헤더로 렌더링
|
||||
- [ ] 카테고리 헤더에 건수(`count`) Badge 표시
|
||||
- [ ] 카테고리 클릭 시 접힘/펼침 토글
|
||||
- [ ] 품목 노드에 타입 뱃지, 코드, 품목명, 수량, 단위 표시
|
||||
- [ ] FG 품목에서만 BOM 섹션 표시 (기존 조건 유지)
|
||||
- [ ] BOM 데이터 없는 품목은 섹션 미표시
|
||||
|
||||
---
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- `changes/20260318_item-management-bom-tree.md` — MNG BOM 트리 3단계 구현 완료 내역
|
||||
|
||||
---
|
||||
|
||||
**최종 업데이트**: 2026-03-18
|
||||
Reference in New Issue
Block a user