feat: [bending] 절곡품 관리 기능 개발 계획서 추가

- README.md: 전체 개요, 메뉴 구조, 작업 순서
- step1-데이터분석.md: 레거시 매핑 + options 확장 스키마
- step2-API.md: 엔드포인트 설계 (docs 규칙 준수)
- step3-MNG화면.md: Blade+HTMX 화면 구성 (3타입별 폼)
- step4-React연동.md: 견적 이미지 + 운영 화면 계획
This commit is contained in:
김보곤
2026-03-16 17:40:47 +09:00
parent 02073d4640
commit 828b452186
5 changed files with 1810 additions and 0 deletions

View File

@@ -0,0 +1,390 @@
# 절곡품 관리 기능 개발 계획서
> **시작일**: 2026-03-16
> **위치**: MNG 생산관리 > 절곡품 관리 (신규 메뉴)
> **목표**: 경동기업(5130) 수준의 절곡품 마스터 관리 + 전개도 데이터 + 이미지 관리
> **원칙**: 기존 BendingInfoBuilder/PrefixResolver 보존, items.options 확장 방식
---
## 배경
SAM은 절곡품의 "계산과 조합"(BendingInfoBuilder/PrefixResolver)은 잘 되어 있지만,
"관리와 시각화"가 빠져 있다. 경동기업(5130) `guiderail/list.php` 수준의 관리 화면을 MNG에 구현한다.
**갭 분석**: `docs/dev/dev_plans/bending-parts-analysis.md` 참조
---
## MNG 현재 구조
### 생산관리 메뉴 (sidebar-static.blade.php)
```
생산 관리 (production-group)
├─ 품목기준 필드 관리 ✅ (구현됨)
├─ 견적수식 관리 ✅ (구현됨)
├─ 제품 관리 (준비중)
├─ 자재 관리 (준비중)
├─ BOM 관리 (준비중)
├─ 카테고리 관리 (준비중)
└─ 절곡품 관리 ← 🆕 추가 대상
├─ 기초관리 (개별 부품 CRUD)
└─ 절곡품 (모델별 조합 관리)
```
### 기존 절곡 관련 코드 (MNG)
| 파일 | 역할 | 변경 여부 |
|------|------|----------|
| `views/documents/partials/bending-worklog.blade.php` | 절곡 작업일지 렌더링 | 무변경 |
| `views/documents/partials/bending-inspection-data.blade.php` | 절곡 중간검사 | 무변경 |
---
## 작업 순서
```
Step 1 (DB분석) → Step 2 (API) → Step 3 (MNG 화면) → Step 4 (React 연동)
레거시 매핑 options 확장 Blade + HTMX 견적 이미지
+ 데이터 정리 + 엔드포인트 + 메뉴 등록
```
상세 계획: 아래 문서 참조
| 문서 | 내용 |
|------|------|
| `step1-데이터분석.md` | 레거시 매핑 + options 확장 |
| `step2-API.md` | API 엔드포인트 + 컨트롤러 설계 |
| `step3-MNG화면.md` | Blade 뷰 + HTMX + 메뉴 등록 |
| `step4-React연동.md` | 견적 페이지 이미지 컴포넌트 |
---
## 참조 문서
| 문서 | 경로 | 용도 |
|------|------|------|
| 갭 분석 | `dev_plans/bending-parts-analysis.md` | 요구사항 기준 |
| API 규칙 | `standards/api-rules.md` | API 네이밍/응답 |
| options 정책 | `standards/options-column-policy.md` | JSON 컬럼 설계 |
| 품목 정책 | `rules/item-policy.md` | BD 코드 체계 |
| Phase 2 | `dev_plans/integrated-phase-2.md` | 절곡 설계 |
| Phase 3 | `dev_plans/integrated-phase-3.md` | 절곡 검사 |
## 프로토타입
| 위치 | 설명 |
|------|------|
| `SAM/work/절곡/index.html` | 사이드바 + iframe 전체 구조 |
| `SAM/work/절곡/base.html` | 기초관리 목록 (참고용) |
| `SAM/work/절곡/base-form.html` | 등록/수정 폼 + 절곡 테이블 (참고용) |
| `SAM/work/절곡/products.html` | 절곡품 탭 목록 (참고용) |
| `SAM/work/절곡/product-form.html` | 절곡품 등록/수정 (참고용) |
# 절곡품 관리 — 전체 흐름도
---
## 1. 시스템 전체 구조
```
┌─────────────────────────────────────────────────────────────────────┐
│ SAM 절곡품 관리 시스템 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ MNG │ │ API │ │ React │ │
│ │ (샘플용) │────→│ (핵심) │←────│ (운영용) │ │
│ │ Blade │ │ Laravel │ │ Next.js │ │
│ └──────────┘ └─────┬────┘ └──────────┘ │
│ │ │
│ ┌────┴────┐ │
│ │ samdb │ │
│ │ items │ ← item_category = 'BENDING' │
│ │ files │ ← field_key = 'bending_diagram' │
│ └─────────┘ │
│ │ │
│ ┌────┴────┐ │
│ │ R2 │ ← Cloudflare (이미지 저장) │
│ └─────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 2. 데이터 구조 (2계층)
```
┌─────────────────────────────────────────────────────────────────┐
│ │
│ [1계층] 기초관리 — 개별 부품 (items 테이블) │
│ ════════════════════════════════════════ │
│ │
│ items (item_category = 'BENDING') │
│ ┌──────────────────────────────────────────────────┐ │
│ │ id: 100 │ │
│ │ code: BD-가이드레일-KSS01-SUS-120*70 │ │
│ │ name: 가이드레일 KSS01 SUS 120*70 │ │
│ │ options: { │ │
│ │ item_name: "마감재" ← 부품 품명 │ │
│ │ item_sep: "스크린" ← 대분류 │ │
│ │ item_bending: "가이드레일" ← 중분류 │ │
│ │ material: "SUS 1.2T" ← 재질 │ │
│ │ model_name: "KSS01" ← 소속 모델 │ │
│ │ model_UA: "인정" ← 인정여부 │ │
│ │ item_spec: "120*70" ← 규격 │ │
│ │ rail_width: 70 ← 레일폭 │ │
│ │ bendingData: [ ← 전개도 데이터 │ │
│ │ {no:1, input:10, rate:"", sum:10, ...}, │ │
│ │ {no:2, input:11, rate:"", sum:21, ...}, │ │
│ │ ... │ │
│ │ ] │ │
│ │ + 케이스전용: exit_direction, box_width, ... │ │
│ │ } │ │
│ └──────────────────────────────────────────────────┘ │
│ ↑ 265건 (레거시) + α
│ │
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
│ │
│ [2계층] 절곡품 — 모델별 부품 조합 │
│ ════════════════════════════════ │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 가이드레일 모델: KSS01 벽면형 SUS마감 │ │
│ │ │ │
│ │ components (부품 조합): │ │
│ │ ┌─────┬───────────┬──────────┬────┬────────┐ │ │
│ │ │순서 │ 부품명 │ 재질 │수량│전개폭합│ │ │
│ │ ├─────┼───────────┼──────────┼────┼────────┤ │ │
│ │ │ 1 │ 마감재 │ SUS 1.2T│ 2 │ 203 │ ──→ item:100│
│ │ │ 2 │ 본체 │ EGI 1.55│ 1 │ 296 │ ──→ item:101│
│ │ │ 3 │ 벽면형-C │ EGI 1.55│ 1 │ 144 │ ──→ item:102│
│ │ │ 4 │ 벽면형-D │ EGI 1.55│ 1 │ 144 │ ──→ item:103│
│ │ └─────┴───────────┴──────────┴────┴────────┘ │ │
│ │ │ │
│ │ 재질별 폭합: SUS 1.2T → 406 | EGI 1.55T → 398 │ │
│ └────────────────────────────────────────────────────────┘ │
│ ↑ 가이드레일 20건 + 케이스 + 하단마감재 │
│ │
└─────────────────────────────────────────────────────────────────┘
```
---
## 3. 3가지 타입 비교
```
┌─────────────────┬─────────────────┬─────────────────┐
│ 가이드레일 │ 케이스 │ 하단마감재 │
├─────────────────┼─────────────────┼─────────────────┤
│ │ │ │
│ 모델: KSS01 │ 모델: ❌ 없음 │ 모델: KSS01 │
│ 마감: SUS/EGI │ 마감: ❌ 없음 │ 마감: SUS/EGI │
│ 형상: 벽면/측면 │ 형상: ❌ 없음 │ 형상: ❌ 없음 │
│ 대분류: 스크린/철재│ 대분류: ❌ │ 대분류: 스크린/철재│
│ 인정: 인정/비인정 │ 인정: ❌ │ 인정: 인정/비인정 │
│ │ │ │
│ 규격: 120×70 │ 규격: 650×550 │ 규격: 60×40 │
│ 레일폭: 70 │ 전면밑: 50 │ │
│ │ 레일폭: 75 │ │
│ │ 점검구: 후면 │ │
│ │ │ │
│ 파트: 3~5개 │ 파트: 5개 │ 파트: 1개 │
│ ┌─────────────┐│ ┌─────────────┐│ ┌─────────────┐│
│ │본체상부 ││ │상부덮개 ││ │하단마감 ││
│ │본체하부 ││ │전면 ││ │(단일 파트) ││
│ │마감재 ││ │점검구 ││ └─────────────┘│
│ │(+C형,D형) ││ │린텔 ││ │
│ └─────────────┘│ │후면코너 ││ │
│ │ └─────────────┘│ │
├─────────────────┼─────────────────┼─────────────────┤
│ 재질별 폭합 │ 재질별 폭합 │ 재질별 폭합 │
│ SUS: 406 │ EGI: 2652 │ SUS: 193 │
│ EGI: 398 │ │ │
└─────────────────┴─────────────────┴─────────────────┘
```
---
## 4. 전개도 테이블 구조 (1개 부품)
```
레거시 5130 화면과 동일한 구조:
┌────────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │
├────────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│ 번호 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │
├────────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│ 입력 │ 10 │ 11 │ 110 │ 30 │ 15 │ 15 │ 15 │ ← 치수 입력
│ │[색상]│ │ │ │ │[색상]│ │ ← 파란 배경
├────────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│ 연신율 │ │ │ -1 │ -1 │ -1 │ │ │ ← 절곡 방향
├────────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│연신율후 │ 10 │ 11 │ 109 │ 29 │ 14 │ 15 │ 15 │ ← input + rate
│ │ │ │(-1) │(-1) │(-1) │ │ │ (rate=-1 → -1mm)
├────────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│ 합계 │ 10 │ 21 │ 130 │ 159 │ 173 │ 188 │ 203 │ ← 보정후 누적합
│ │[색상]│ │ │ │ │[색상]│ │ ← 노란 배경
├────────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│ 음영 │ ■■ │ │ │ │ │ ■■ │ │ ← 색상 마킹
├────────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│ A각 │ │ │ │ A각 │ │ │ │ ← A각 표시
└────────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
연신율 보정 규칙:
rate = "" → 보정 없음 (input 그대로)
rate = "-1" → input - 1mm (하향 절곡)
rate = "1" → input + 1mm (상향 절곡)
합계 = 보정후 값의 누적합
폭합계 = 마지막 합계값 (이 예시: 203)
```
---
## 5. JSON 저장 구조 (options.bendingData)
```
레거시 (별도 배열 5개) SAM (객체 배열 1개)
──────────────────── ────────────────────
inputList: [10,11,110...] bendingData: [
bendingrateList: ["","","-1"...] { no:1, input:10, rate:"",
sumList: [10,21,130...] sum:10, color:true, aAngle:false },
colorList: [true,false,false...] { no:2, input:11, rate:"",
AList: [false,false,false...] sum:21, color:false, aAngle:false },
{ no:3, input:110, rate:"-1",
→ 5개 배열 동기화 필요 sum:130, color:false, aAngle:false },
→ 열 추가/삭제 시 5개 다 조작 ...
]
→ 1개 배열만 관리
→ 열 추가 = 객체 1개 push
```
---
## 6. 화면 흐름도
```
┌──────────────────────────────────────────────────────────────┐
│ MNG 사이드바 │
│ │
│ 생산 관리 │
│ ├─ 품목기준 필드 관리 │
│ ├─ 견적수식 관리 │
│ └─ 🆕 절곡품 관리 │
│ ├─ 기초관리 ─────────────────┐ │
│ └─ 절곡품 ──────────────┐ │ │
│ │ │ │
└─────────────────────────────┼────┼───────────────────────────┘
│ │
┌───────────────────┘ └───────────────────┐
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ 절곡품 목록 │ │ 기초관리 목록 │
│ │ │ │
│ [가이드레일] [케이스] │ │ 265건 테이블 │
│ [하단마감재] │ │ 필터: 대분류/인정/ │
│ │ │ 그룹/품명/검색 │
│ 필터 + 테이블 │ │ │
│ │ │ 행 클릭 ──→ 상세 │
│ 행 클릭 ──→ 상세 │ │ [+등록] ──→ 등록 │
│ [+등록] ──→ 등록 │ └───────────┬──────────┘
└───────────┬──────────┘ │
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ 절곡품 등록/수정 │ │ 기초관리 등록/수정 │
│ │ │ │
│ ┌──────────┬───────┐ │ │ ┌──────────┬───────┐ │
│ │ 기본정보 │ 이미지 │ │ │ │ 기본정보 │ 이미지 │ │
│ │ (타입별) │ 업로드 │ │ │ │ 대분류 │ 업로드 │ │
│ ├──────────┤ 검색어 │ │ │ │ 그룹/품명 │ 검색어 │ │
│ │ 파트 탭 │ │ │ │ │ 재질/규격 │ │ │
│ │ [1][2][3] │ │ │ │ ├──────────┤ │ │
│ │ │ │ │ │ │ 절곡 테이블│ │ │
│ │ 절곡테이블│ │ │ │ │ (단일) │ │ │
│ │ (파트별) │ │ │ │ ├──────────┤ │ │
│ ├──────────┤ │ │ │ │ 재질별폭합│ │ │
│ │ 재질별폭합│ │ │ │ └──────────┴───────┘ │
│ └──────────┴───────┘ │ └──────────────────────┘
└──────────────────────┘
```
---
## 7. API 엔드포인트 흐름
```
MNG / React
├── GET /api/v1/bending-items ← 기초관리 목록
├── GET /api/v1/bending-items/filters ← 필터 옵션
├── GET /api/v1/bending-items/{id} ← 상세
├── POST /api/v1/bending-items ← 등록
├── PUT /api/v1/bending-items/{id} ← 수정
├── DELETE /api/v1/bending-items/{id} ← 삭제
├── GET /api/v1/guiderail-models ← 절곡품 모델 목록
├── GET /api/v1/guiderail-models/{id} ← 모델 상세 (부품조합)
├── POST /api/v1/guiderail-models ← 모델 등록
├── PUT /api/v1/guiderail-models/{id} ← 모델 수정
├── DELETE /api/v1/guiderail-models/{id} ← 모델 삭제
├── POST /api/v1/items/{id}/files ← 이미지 업로드 (기존)
├── GET /api/v1/items/{id}/files ← 이미지 목록 (기존)
└── GET /api/v1/files/{id}/view ← 이미지 표시 (기존)
※ 이미지는 기존 ItemsFileController 재사용
※ field_key: 'bending_diagram'
```
---
## 8. 작업 순서
```
Step 1 Step 2 Step 3 Step 4
데이터 분석 API 구현 MNG 화면 React 화면
━━━━━━━━ ━━━━━━━━ ━━━━━━━━ ━━━━━━━━
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│레거시 265건│ │Controller│ │기초관리 │ │견적 이미지│
│SAM 170건 │──→ │Service │──→ │ 목록/등록 │──→ │GuiderailP│
│매핑 테이블 │ │FormReq │ │절곡품 │ │review │
│ │ │Resource │ │ 목록/등록 │ │ │
│options 확장│ │ │ │ │ │절곡품 │
│artisan cmd│ │이미지: │ │메뉴 등록 │ │관리 화면 │
│ │ │기존 재사용│ │(tinker) │ │(본 화면) │
│회귀 테스트 │ │ │ │ │ │ │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
API 프로젝트 API 프로젝트 MNG 프로젝트 React 프로젝트
(샘플 확인용) (운영용)
```
---
## 9. 레거시 → SAM 대응표
```
레거시 (5130) SAM
━━━━━━━━━━━━━ ━━━━━
chandj.bending (265건) → items (item_category='BENDING') + options
chandj.guiderail (20건) → guiderail-models API (신규 저장 구조)
guiderail/list.php → MNG /bending/products (절곡품 목록)
bending CRUD → MNG /bending/base (기초관리)
put_guiderail_image.php → 기존 ItemsFileController (R2)
fetch_guiderail_detail.php → React GuiderailPreview
drawingTool.js (Canvas) → 2차 구현 (1차는 이미지 업로드만)
inputList[] (별도 배열) → bendingData[] (객체 배열)
bendingrateList[] → bendingData[].rate
sumList[] → bendingData[].sum
colorList[] → bendingData[].color
AList[] → bendingData[].aAngle
```

View File

@@ -0,0 +1,282 @@
# Step 1: 데이터 분석 + options 확장
> **프로젝트**: API (`sam/api`)
> **선행 조건**: 없음
> **참조**: `standards/options-column-policy.md`, `rules/item-policy.md`
---
## 1. 레거시 데이터 매핑
### 1-1. 레거시 데이터 현황 (chandj DB)
**테이블별 건수**:
| 테이블 | 건수 | 설명 |
|--------|------|------|
| `bending` | 265건 (활성) | 개별 절곡품 부품 |
| `guiderail` | 20건 (활성) | 모델별 부품 조합 (7개 모델 + 비인정 1개) |
| `bendingfee` | 82건 | 절곡 단가 |
| `bendingmap` | 9건 | 절곡 매핑 |
| `etcbending` | 2건 | 기타 절곡 |
| `bending_work_log` | 8건 | 작업 로그 |
**bending 분류 분포** (265건):
| 대분류 | 중분류 | 건수 |
|--------|--------|------|
| 스크린 | 가이드레일 | 41 |
| 스크린 | 케이스 | 24 |
| 스크린 | 하단마감재 | 6 |
| 스크린 | 마구리 | 4 |
| 스크린 | L-BAR | 2 |
| 철재 | 케이스 | 136 |
| 철재 | 가이드레일 | 30 |
| 철재 | (미분류) | 9 |
| 철재 | 마구리 | 8 |
| 철재 | 하단마감재 | 5 |
**bending 테이블 전체 컬럼** (25개):
| 컬럼 | 타입 | 설명 | options 키 |
|------|------|------|-----------|
| `num` | int PK | 번호 | `legacy_bending_num` |
| `is_deleted` | tinyint | 삭제여부 | SAM 자체 관리 |
| `item_sep` | varchar(14) | 대분류 (스크린/철재) | `item_sep` |
| `model_UA` | varchar(15) | 인정여부 | `model_UA` |
| `item_bending` | varchar(40) | 중분류 | `item_bending` |
| `itemName` | varchar(50) | 품명 | `item_name` |
| `material` | varchar(25) | 재질 | `material` |
| `parentnum` | varchar(12) | 부모 참조 | `parent_num` |
| `registration_date` | date | 등록일 | `registration_date` |
| `imgdata` | text | 이미지 파일경로 | `image_path` |
| `inputList` | text | 치수 JSON | `bendingData[].input` |
| `bendingrateList` | text | 연신율 JSON | `bendingData[].rate` |
| `sumList` | text | 합계 JSON | `bendingData[].sum` |
| `colorList` | text | 색상마킹 JSON | `bendingData[].color` |
| `AList` | text | A각 JSON | `bendingData[].aAngle` |
| `memo` | text | 비고 | `memo` |
| `update_log` | text | 수정 이력 | SAM 자체 관리 (updated_at) |
| `item_spec` | varchar(50) | 규격 | `item_spec` |
| `widthsum` | int | 폭합계 | 계산값 (bendingData 마지막 sum) |
| `author` | varchar(20) | 작성자 | `author` |
| `search_keyword` | varchar(50) | 검색어 | `search_keyword` |
| `exit_direction` | varchar(20) | 점검구 방향 (케이스) | `exit_direction` |
| `front_bottom_width` | varchar(5) | 전면부 밑 치수 (케이스) | `front_bottom_width` |
| `rail_width` | varchar(5) | 레일폭 | `rail_width` |
| `box_width` | varchar(5) | 케이스 너비 | `box_width` |
| `box_height` | varchar(5) | 케이스 높이 | `box_height` |
**전개도 JSON 현황**: 265건 전부 inputList/bendingrateList/sumList/colorList/imgdata 보유 (크기: 30~50 bytes/필드)
**guiderail 모델 목록** (20건):
| 모델 | 인정 | 형태 | 레일폭 | 레일길이 | 마감 | 제품 |
|------|------|------|--------|---------|------|------|
| KSS01 | 인정 | 벽면/측면 | 70/120 | 120 | SUS | 스크린 |
| KSS02 | 인정 | 벽면/측면 | 70/120 | 120 | SUS | 스크린 |
| KSE01 | 인정 | 벽면/측면 | 70/120 | 120 | EGI/SUS | 스크린 |
| KWE01 | 인정 | 벽면/측면 | 70/120 | 120 | EGI/SUS | 스크린 |
| KTE01 | 인정 | 벽면/측면 | 75/125 | 130 | EGI/SUS | 철재 |
| KQTS01 | 인정 | 벽면/측면 | 75/125 | 130 | SUS | 철재 |
| KDSS01 | 인정 | 벽면 | 150 | 150 | SUS | 스크린 |
| 스크린비인정 | 비인정 | 벽면 | 70 | 130 | SUS | 스크린 |
**guiderail 테이블 전체 컬럼** (15개):
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `num` | int PK | 번호 |
| `is_deleted` | varchar(2) | 삭제여부 |
| `registration_date` | date | 등록일 |
| `model_UA` | varchar(10) | 인정여부 |
| `check_type` | varchar(20) | 형상 (벽면형/측면형) |
| `model_name` | varchar(15) | 모델명 |
| `author` | varchar(50) | 작성자 |
| `remark` | text | 비고 |
| `update_log` | text | 수정 이력 |
| `rail_width` | varchar(10) | 레일폭 |
| `rail_length` | varchar(10) | 레일길이(높이) |
| `finishing_type` | varchar(10) | 마감 (SUS마감/EGI마감) |
| `bending_components` | mediumtext | 부품 조합 JSON |
| `firstitem` | varchar(15) | 대분류 (스크린/철재) |
| `search_keyword` | varchar(50) | 검색어 |
| `material_summary` | text | 재질별 폭합 |
### 1-1b. 추가 파악 필요
| 작업 | 대상 | 비고 |
|------|------|------|
| BendingInfoBuilder.php 지원 모델 추출 | 코드 분석 | 기존 로직 파악 |
| PrefixResolver.php PREFIX 규칙 추출 | 코드 분석 | 기존 로직 파악 |
| bendingfee 82건 구조 | 절곡 단가 — SAM 연동 필요 여부 | |
| bendingmap 9건 구조 | 매핑 용도 확인 | |
### 1-2. SAM BD 품목 현황 (170건)
**패턴별 분류**:
| 패턴 | 건수 | 예시 | 파싱 |
|------|------|------|------|
| A) `BD-PREFIX-LEN` | 112 | BD-RS-30, BD-CF-35 | prefix/length 자동 추출 |
| B) `BD-L-BAR-모델-규격` | 5 | BD-L-BAR-KSS01-17*60 | 모델+규격 추출 가능 |
| C) `BD-가이드레일-모델-재질-규격` | 21 | BD-가이드레일-KSS01-SUS-120*70 | 모델+재질+규격 추출 가능 |
| D) `BD-마구리-규격` | 10 | BD-마구리-655*505 | 규격 추출 가능 |
| E) `BD-케이스-규격` | 11 | BD-케이스-650*550 | 규격 추출 가능 |
| F) `BD-하단마감재-모델-재질-규격` | 10 | BD-하단마감재-KSS01-SUS-60*40 | 모델+재질+규격 추출 가능 |
| G) `BD-보강평철-규격` | 1 | BD-보강평철-50 | 규격 추출 가능 |
**A) PREFIX-LEN 112건 상세**:
| PREFIX | 용도 | 길이 종류 | 건수 |
|--------|------|----------|------|
| RS | 가이드레일 마감재(벽면) SUS | 24,30,35,40,43 | 5 |
| SS | 가이드레일 마감재(측면) SUS | 30,35,40,43 | 4 |
| SU | 가이드레일 마감재(측면) SUS2 | 30,35,40,43 | 4 |
| RM | 가이드레일 본체(벽면) | 12,24,30,35,40,42,43 | 7 |
| SM | 가이드레일 본체(측면) | 02,24,30,35,40,43 | 6 |
| RC | 가이드레일 C형(벽면) | 12,24,30,35,40,42,43 | 7 |
| RD | 가이드레일 D형(벽면) | 12,24,30,35,40,42,43 | 7 |
| SC | 가이드레일 C형(측면) | 24,30,35,40,43 | 5 |
| SD | 가이드레일 D형(측면) | 24,30,35,40,43 | 5 |
| RT | 가이드레일 본체(벽면/철재) | 30,43 | 2 |
| ST | 가이드레일 본체(측면/철재) | 43 | 1 |
| BS | 하단마감재(스크린) SUS | 12,24,30,35,40,42,43 | 7 |
| BE | 하단마감재(스크린) EGI | 30,40 | 2 |
| TS | 하단마감재(철재) SUS | 40,43 | 2 |
| CF | 케이스 전면부 | 12,24,30,35,40,41 | 6 |
| CL | 케이스 린텔부 | 12,24,30,35,40,41 | 6 |
| CP | 케이스 점검구 | 12,24,30,35,40,41 | 6 |
| CB | 케이스 후면코너부 | 12,24,30,35,40,41 | 6 |
| GI | 연기차단재 | 24,30,35,40,43,53,54,83,84 | 9 |
| HH | 보강평철 | 30,40 | 2 |
| LA | L-Bar | 30,40 | 2 |
| XX | 하부BASE/상부덮개/마구리(공용) | 12,24,30,35,40,41,43 | 7 |
| YY | 별도마감 | 30,35,40,43 | 4 |
**options 채워진 상태**:
| 상태 | 건수 | 비고 |
|------|------|------|
| options 완전 (prefix+length) | 22 | 13% |
| options 있지만 불완전 | 90 | PREFIX-LEN 중 일부 |
| options 비어있음 (`{}`) | 58 | 한글 패턴 전부 |
### 1-3. 매핑 테이블 작성
```
레거시 bending (부품 단위) SAM items (품목 단위)
───────────────────────── ─────────────────────
num:100 마감재 SUS 120*70 ↔ BD-가이드레일-KSS01-SUS-120*70 (한글 패턴)
num:101 본체 EGI 120*70 ↔ BD-RM-30 (PREFIX-LEN — 길이 기준)
※ 부품 단위 vs 길이 단위 구조 차이
```
**핵심 결정사항**:
- BD-한글 패턴(58건)을 BD-PREFIX-LEN으로 통일할지
- 레거시 265건 중 SAM에 없는 항목 → 신규 생성 범위
- 매핑 애매한 항목 → 사업부 확인 목록
---
## 2. options 확장
### 2-1. 확장 스키마
**기존 키 (보존)**:
```json
{
"source": "bending_item_seeder",
"lot_managed": true,
"consumption_method": "auto",
"production_source": "self_produced",
"input_tracking": true,
"prefix": "RS",
"length_code": "30",
"length_mm": 3000
}
```
**추가 키**:
```json
{
// --- 기본 속성 ---
"item_name": "마감재", // 품명 (레거시 itemName — items.name과 별도 보존)
"item_sep": "스크린", // 대분류 (스크린/철재)
"item_bending": "가이드레일", // 중분류 (가이드레일/케이스/하단마감재/마구리/L-BAR)
"item_spec": "120*70", // 규격
"material": "SUS 1.2T", // 재질
"model_name": "KSS01", // 모델명
"model_UA": "인정", // 인정여부
"search_keyword": "", // 검색 키워드
"rail_width": 70, // 레일폭
"registration_date": "2025-07-19", // 등록일
"author": "개발자", // 작성자
"memo": "", // 비고
"parent_num": null, // 부모 절곡품 참조 (조합 관계)
// --- 케이스 전용 ---
"exit_direction": "후면 점검구", // 점검구 방향 (후면/양면/밑면)
"front_bottom_width": 50, // 전면부 밑 치수 (mm)
"box_width": 650, // 케이스 너비 (mm)
"box_height": 550, // 케이스 높이 (mm)
// --- 전개도 데이터 (인덱스 기반 객체 배열) ---
"bendingData": [
{ "no": 1, "input": 10, "rate": "", "sum": 10, "color": true, "aAngle": false },
{ "no": 2, "input": 11, "rate": "", "sum": 21, "color": false, "aAngle": false }
// ... 열 단위로 모든 속성을 하나의 객체에 통합
],
// --- 이미지/추적 ---
"image_path": "", // 전개도 이미지 경로
"legacy_bending_num": null // 레거시 추적용
}
```
### 2-2. 마이그레이션 순서
```
1단계: 기존 148건 prefix/length 채우기
→ BD-PREFIX-LEN 패턴에서 자동 추출
2단계: 레거시 속성 입력
→ 매핑 테이블 기반 item_sep/item_bending/material 등
3단계: 전개도 JSON 입력
→ 레거시 inputList/bendingrateList/sumList/colorList
```
### 2-3. artisan command
```bash
# 1단계
php artisan bending:fill-options --dry-run # 미리보기
php artisan bending:fill-options # 실행
# 2단계
php artisan bending:import-legacy --dry-run
php artisan bending:import-legacy
# 3단계 (2단계에 포함 가능)
```
---
## 3. 회귀 테스트 (필수)
| 테스트 | 확인 내용 | 판정 기준 |
|--------|----------|----------|
| BendingInfoBuilder | 견적 BOM 계산 결과 | 변경 전/후 동일 |
| PrefixResolver | BD 코드 자동 결정 | 변경 전/후 동일 |
| 작업지시서 절곡 섹션 | GuideRailSection 렌더링 | 정상 표시 |
| 절곡 검사 | inspection-config API | 정상 응답 |
| 견적→수주→작업지시 | 전체 흐름 1건 | 오류 없음 |
---
## 4. 산출물
- [ ] 매핑 테이블 (레거시 num ↔ SAM item_id)
- [ ] artisan command (bending:fill-options, bending:import-legacy)
- [ ] 회귀 테스트 결과

View File

@@ -0,0 +1,525 @@
# Step 2: API 엔드포인트
> **프로젝트**: API (`sam/api`)
> **선행 조건**: Step 1 완료
> **참조**: `standards/api-rules.md`, `standards/options-column-policy.md`, `rules/item-policy.md`
---
## 1. 설계 방침
### 기존 규칙 준수 사항
| 규칙 | 적용 |
|------|------|
| URL prefix | `/api/v1/` |
| 응답 형식 | `ApiResponse::handle()``{success, message, data}` |
| Controller | FormRequest 타입힌트 → Service 호출만 |
| Service | `extends Service`, `tenantId()`, `apiUserId()` 사용 |
| i18n 메시지 | `__('message.bending_item.created')` 패턴 |
| 멀티테넌시 | `BelongsToTenant` 글로벌 스코프 |
| Audit 로그 | `audit_logs` 테이블 자동 기록 |
| SoftDeletes | 기본 적용 |
| options | `'array'` 캐스트, `getOption()`/`setOption()` 헬퍼 |
| Validation | FormRequest 클래스, 컨트롤러에서 직접 validate() 금지 |
### 기존 Item 구조와의 관계
```
기존 구조:
ItemsController → ItemsService → items 테이블
item_type: FG(완제품), PT(부품), SM(부자재), RM(원자재), CS(소모품)
item_category: 'BENDING' (절곡품 구분)
절곡품 API 방향:
→ 기존 ItemsController 무변경
→ 별도 BendingItemController 생성 (items 테이블을 item_category='BENDING'으로 필터)
→ 절곡품 전용 필터/검색/전개도 데이터 관리
```
---
## 2. 엔드포인트 설계
### 2-1. 절곡품 기초관리 (개별 부품)
| Method | Path | 설명 | 비고 |
|--------|------|------|------|
| GET | `/api/v1/bending-items` | 목록 (필터/검색/페이지네이션) | |
| GET | `/api/v1/bending-items/filters` | 필터 옵션 (분류/재질/모델 distinct) | 캐시 10분 |
| GET | `/api/v1/bending-items/{id}` | 상세 (options 전체) | |
| POST | `/api/v1/bending-items` | 등록 | |
| PUT | `/api/v1/bending-items/{id}` | 수정 | |
| DELETE | `/api/v1/bending-items/{id}` | 삭제 (soft delete) | |
| ~~이미지~~ | 기존 `ItemsFileController` 사용 | `field_key: 'bending_diagram'` | 별도 엔드포인트 불필요 |
**필터 파라미터** (GET /api/v1/bending-items):
```
?item_sep=스크린 # 대분류
&item_bending=가이드레일 # 중분류
&material=SUS # 재질 (부분 매칭)
&model_UA=인정 # 인정여부
&search=KSS01 # 통합 검색 (이름/검색어/규격)
&page=1&size=50 # 페이지네이션 (size — api-rules 기준)
```
### 2-2. 절곡품 모델 관리 (조합)
| Method | Path | 설명 | 비고 |
|--------|------|------|------|
| GET | `/api/v1/guiderail-models` | 모델 목록 (타입별) | ?type=가이드레일 |
| GET | `/api/v1/guiderail-models/{id}` | 모델 상세 (부품 조합 + 재질별 폭합) | |
| POST | `/api/v1/guiderail-models` | 모델 등록 | |
| PUT | `/api/v1/guiderail-models/{id}` | 모델 수정 | |
| DELETE | `/api/v1/guiderail-models/{id}` | 모델 삭제 (soft delete) | |
---
## 3. 구현 파일 구조
### Controller
```
app/Http/Controllers/Api/V1/
├─ BendingItemController.php ← 신규
└─ GuiderailModelController.php ← 신규
```
```php
// BendingItemController.php
class BendingItemController extends Controller
{
public function __construct(private BendingItemService $service) {}
public function index(BendingItemIndexRequest $request)
{
return ApiResponse::handle(fn() =>
$this->service->list($request->validated())
);
}
public function store(BendingItemStoreRequest $request)
{
return ApiResponse::handle(fn() =>
$this->service->create($request->validated()),
__('message.bending_item.created')
);
}
public function show(int $id)
{
return ApiResponse::handle(fn() =>
$this->service->find($id)
);
}
public function update(BendingItemUpdateRequest $request, int $id)
{
return ApiResponse::handle(fn() =>
$this->service->update($id, $request->validated()),
__('message.bending_item.updated')
);
}
public function destroy(int $id)
{
return ApiResponse::handle(fn() =>
$this->service->delete($id),
__('message.bending_item.deleted')
);
}
}
```
### Service
```
app/Services/
├─ BendingItemService.php ← 신규
└─ GuiderailModelService.php ← 신규
```
```php
// BendingItemService.php
class BendingItemService extends Service
{
public function list(array $params): LengthAwarePaginator
{
return Item::where('item_category', 'BENDING')
->when($params['item_sep'] ?? null, fn($q, $v) =>
$q->where('options->item_sep', $v))
->when($params['item_bending'] ?? null, fn($q, $v) =>
$q->where('options->item_bending', $v))
->when($params['material'] ?? null, fn($q, $v) =>
$q->where('options->material', 'like', "%{$v}%"))
->when($params['model_UA'] ?? null, fn($q, $v) =>
$q->where('options->model_UA', $v))
->when($params['search'] ?? null, fn($q, $v) =>
$q->where(fn($q2) => $q2
->where('name', 'like', "%{$v}%")
->orWhere('options->search_keyword', 'like', "%{$v}%")
->orWhere('options->item_spec', 'like', "%{$v}%")))
->orderByDesc('id')
->paginate($params['size'] ?? 50);
}
public function create(array $data): Item
{
$options = $this->buildOptions($data);
$item = Item::create([
'tenant_id' => $this->tenantId(),
'item_type' => 'PT',
'item_category' => 'BENDING',
'code' => $data['code'],
'name' => $data['name'],
'options' => $options,
'created_by' => $this->apiUserId(),
]);
// audit log 자동 기록
return $item;
}
public function update(int $id, array $data): Item
{
$item = Item::findOrFail($id);
// setOption()으로 개별 키 업데이트 (기존 키 보존)
foreach ($data as $key => $value) {
if (in_array($key, ['code', 'name'])) {
$item->$key = $value;
} else {
$item->setOption($key, $value);
}
}
$item->updated_by = $this->apiUserId();
$item->save();
return $item;
}
private function buildOptions(array $data): array
{
$options = [];
$optionKeys = [
'item_name', 'item_sep', 'item_bending', 'item_spec',
'material', 'model_name', 'model_UA', 'search_keyword',
'rail_width', 'registration_date', 'author', 'memo',
'parent_num', 'exit_direction', 'front_bottom_width',
'box_width', 'box_height', 'bendingData', 'image_path',
];
foreach ($optionKeys as $key) {
if (isset($data[$key])) {
$options[$key] = $data[$key];
}
}
return $options ?: null;
}
}
```
### FormRequest
```
app/Http/Requests/Api/V1/
├─ BendingItemIndexRequest.php ← 신규
├─ BendingItemStoreRequest.php ← 신규
├─ BendingItemUpdateRequest.php ← 신규
├─ GuiderailModelStoreRequest.php ← 신규
└─ GuiderailModelUpdateRequest.php← 신규
```
```php
// BendingItemStoreRequest.php
class BendingItemStoreRequest extends FormRequest
{
public function rules(): array
{
return [
'code' => 'required|string|max:100|unique:items,code',
'name' => 'required|string|max:200',
'item_name' => 'required|string|max:50',
'item_sep' => 'required|in:스크린,철재',
'item_bending' => 'required|string',
'material' => 'required|string',
'model_UA' => 'nullable|in:인정,비인정',
'item_spec' => 'nullable|string',
'model_name' => 'nullable|string',
'search_keyword' => 'nullable|string',
'rail_width' => 'nullable|integer',
'memo' => 'nullable|string',
// 케이스 전용
'exit_direction' => 'nullable|string',
'front_bottom_width' => 'nullable|integer',
'box_width' => 'nullable|integer',
'box_height' => 'nullable|integer',
// 전개도 데이터
'bendingData' => 'nullable|array',
'bendingData.*.no' => 'required|integer',
'bendingData.*.input' => 'required|numeric',
'bendingData.*.rate' => 'nullable|string',
'bendingData.*.sum' => 'required|numeric',
'bendingData.*.color' => 'required|boolean',
'bendingData.*.aAngle' => 'required|boolean',
];
}
}
```
### Resource
```
app/Http/Resources/Api/V1/
├─ BendingItemResource.php ← 신규
└─ GuiderailModelResource.php ← 신규
```
```php
// BendingItemResource.php
class BendingItemResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'code' => $this->code,
'name' => $this->name,
// options → 최상위로 풀어서 노출
'item_name' => $this->getOption('item_name'),
'item_sep' => $this->getOption('item_sep'),
'item_bending' => $this->getOption('item_bending'),
'item_spec' => $this->getOption('item_spec'),
'material' => $this->getOption('material'),
'model_name' => $this->getOption('model_name'),
'model_UA' => $this->getOption('model_UA'),
'search_keyword' => $this->getOption('search_keyword'),
'rail_width' => $this->getOption('rail_width'),
'registration_date' => $this->getOption('registration_date'),
'author' => $this->getOption('author'),
'memo' => $this->getOption('memo'),
// 케이스 전용
'exit_direction' => $this->getOption('exit_direction'),
'front_bottom_width' => $this->getOption('front_bottom_width'),
'box_width' => $this->getOption('box_width'),
'box_height' => $this->getOption('box_height'),
// 전개도
'bendingData' => $this->getOption('bendingData'),
'image_path' => $this->getOption('image_path'),
// 계산값
'width_sum' => $this->getWidthSum(),
'bend_count' => $this->getBendCount(),
'has_image' => !empty($this->getOption('image_path')),
// 메타
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
private function getWidthSum(): ?int
{
$data = $this->getOption('bendingData', []);
if (empty($data)) return null;
return (int) end($data)['sum'] ?? null;
}
private function getBendCount(): int
{
$data = $this->getOption('bendingData', []);
return count(array_filter($data, fn($d) => ($d['rate'] ?? '') !== ''));
}
}
```
### 라우트
```php
// routes/api.php (v1 그룹 내부에 추가)
Route::prefix('v1')->middleware(['auth:sanctum'])->group(function () {
// ... 기존 라우트 유지 ...
// 절곡품 기초관리
Route::apiResource('bending-items', BendingItemController::class);
Route::get('bending-items/filters', [BendingItemController::class, 'filters']);
Route::post('bending-items/{id}/image', [BendingItemController::class, 'uploadImage']);
Route::delete('bending-items/{id}/image', [BendingItemController::class, 'deleteImage']);
// 절곡품 모델 (가이드레일 조합)
Route::apiResource('guiderail-models', GuiderailModelController::class);
});
```
---
## 4. 응답 형식
### 목록 응답 (GET /api/v1/bending-items)
```json
{
"success": true,
"message": null,
"data": {
"data": [
{
"id": 123,
"code": "BD-가이드레일-KSS01-SUS-120*70",
"name": "가이드레일 KSS01 SUS 120*70",
"item_name": "마감재",
"item_sep": "스크린",
"item_bending": "가이드레일",
"item_spec": "120*70",
"material": "SUS 1.2T",
"model_name": "KSS01",
"model_UA": "인정",
"width_sum": 203,
"bend_count": 3,
"has_image": true
}
],
"current_page": 1,
"total": 170,
"per_page": 50
}
}
```
### 모델 상세 응답 (GET /api/v1/guiderail-models/{id})
```json
{
"success": true,
"message": null,
"data": {
"id": 1,
"model_name": "KSS01",
"check_type": "벽면형",
"rail_width": 70,
"rail_length": 120,
"finishing_type": "SUS마감",
"item_sep": "스크린",
"model_UA": "인정",
"components": [
{
"order": 1,
"name": "1번(마감재)",
"material": "SUS 1.2T",
"qty": 2,
"bending_item_id": 100,
"sum_total": 203,
"bendingData": [...]
}
],
"material_summary": {
"SUS 1.2T": 406,
"EGI 1.55T": 398
}
}
}
```
---
## 5. 이미지 처리 (Cloudflare R2)
### 기존 파일 시스템 구조
SAM API는 **Cloudflare R2** (S3 호환)로 파일을 관리한다. 절곡품 이미지도 동일한 구조를 따른다.
```
기존 구조:
FileStorageService.php → Storage::disk('r2')->put()
FileStorageController → POST /api/v1/files/upload (임시)
ItemsFileController → POST /api/v1/items/{id}/files (품목 전용)
File 모델 → files 테이블 (메타데이터)
경로 패턴:
임시: {tenant_id}/temp/{year}/{month}/{stored_name}
확정: {tenant_id}/items/{year}/{month}/{stored_name}
```
### 절곡품 이미지 업로드 방안
**기존 `ItemsFileController` 재사용** (별도 이미지 컨트롤러 불필요):
```php
// 이미 존재하는 엔드포인트 활용
POST /api/v1/items/{id}/files 절곡품 이미지 업로드
GET /api/v1/items/{id}/files 이미지 목록
DELETE /api/v1/items/{id}/files/{fileId} 이미지 삭제
// field_key로 절곡품 이미지 구분
field_key: 'bending_diagram' 전개도 이미지
```
### R2 설정 (이미 구성됨)
```php
// config/filesystems.php
'r2' => [
'driver' => 's3',
'key' => env('R2_ACCESS_KEY_ID'),
'secret' => env('R2_SECRET_ACCESS_KEY'),
'region' => 'auto',
'bucket' => 'sam',
'endpoint' => env('R2_ENDPOINT'),
'use_path_style_endpoint' => true,
],
```
### 이미지 조회
```php
// File 모델의 download() 메서드로 스트리밍
GET /api/v1/files/{id}/view 인라인 표시 (브라우저)
GET /api/v1/files/{id}/download 다운로드
```
### 주의사항
- ❌ 별도 이미지 엔드포인트 생성 불필요 — `ItemsFileController` 재사용
- ❌ 로컬 `storage/app/public/bending/` 직접 저장 금지 — R2 사용
-`field_key: 'bending_diagram'`으로 전개도 이미지 식별
-`files` 테이블에 메타데이터 자동 관리 (tenant_id, file_path, mime_type 등)
- ✅ options에는 `image_path` 대신 `file_id` 참조 또는 `field_key`로 조회
---
## 6. options 상수 정의
```php
// Item 모델에 추가 (또는 별도 상수 클래스)
class Item extends Model
{
// 절곡품 options 키 상수
const OPTION_ITEM_NAME = 'item_name';
const OPTION_ITEM_SEP = 'item_sep';
const OPTION_ITEM_BENDING = 'item_bending';
const OPTION_ITEM_SPEC = 'item_spec';
const OPTION_MATERIAL = 'material';
const OPTION_MODEL_NAME = 'model_name';
const OPTION_MODEL_UA = 'model_UA';
const OPTION_SEARCH_KEYWORD = 'search_keyword';
const OPTION_RAIL_WIDTH = 'rail_width';
const OPTION_BENDING_DATA = 'bendingData';
const OPTION_IMAGE_PATH = 'image_path';
const OPTION_EXIT_DIRECTION = 'exit_direction';
const OPTION_BOX_WIDTH = 'box_width';
const OPTION_BOX_HEIGHT = 'box_height';
const OPTION_FRONT_BOTTOM_WIDTH = 'front_bottom_width';
const OPTION_MEMO = 'memo';
const OPTION_AUTHOR = 'author';
const OPTION_REGISTRATION_DATE = 'registration_date';
const OPTION_PARENT_NUM = 'parent_num';
}
```
---
## 7. 주의사항
- ✅ 기존 `ItemsController` / `ItemsService` 무변경
- ✅ items 테이블 스키마 무변경 — options JSON만 활용
-`item_category = 'BENDING'` 필터로 기존 items API 영향 없음
-`setOption()`으로 개별 키 업데이트 — 기존 키 보존
-`ApiResponse::handle()` 사용 — 직접 JSON 반환 금지
- ✅ FormRequest에서만 유효성 검증 — 컨트롤러 validate() 금지
- ✅ i18n 메시지 키 사용 — 직접 문자열 금지
- ✅ SoftDeletes 적용
- ⚠️ `BendingInfoBuilder` / `PrefixResolver` 무변경

View File

@@ -0,0 +1,524 @@
# Step 3: MNG 관리 화면 (Blade + HTMX)
> **프로젝트**: MNG (`sam/mng`)
> **선행 조건**: Step 2 (API 엔드포인트) 완료
> **참조**: 프로토타입 `SAM/work/절곡/`, MNG 기존 Blade 패턴
---
## 1. 메뉴 구조
### 생산관리 하위에 추가
```
생산 관리 — DB menus 테이블 (동적 메뉴)
├─ 품목기준 필드 관리 ✅
├─ 견적수식 관리 ✅
├─ 제품 관리 (준비중)
├─ 자재 관리 (준비중)
├─ BOM 관리 (준비중)
├─ 카테고리 관리 (준비중)
└─ 🆕 절곡품 관리 ← tinker로 menus 테이블에 추가
├─ 기초관리 (/bending/base) ← 개별 부품 CRUD
└─ 절곡품 (/bending/products) ← 모델별 조합 관리
```
### 메뉴 등록 방법
⚠️ **시더 실행 금지** — tinker로 수동 등록
⚠️ **sidebar-static.blade.php 사용 안 함** — 현재 레이아웃은 동적 사이드바(`partials/sidebar.blade.php`) 사용
MNG 사이드바는 DB `menus` 테이블 기반 동적 메뉴 시스템.
`<x-sidebar.menu-tree :menus="$mainMenus" />` 컴포넌트로 렌더링됨.
#### tinker로 메뉴 추가 (서버에서 실행)
```bash
ssh sam-server "cd /home/webservice/mng && php artisan tinker --execute=\"
// 1. 생산관리 부모 메뉴 ID 확인
\\\$parent = App\\\\Models\\\\Commons\\\\Menu::withoutGlobalScopes()
->where('tenant_id', 1)
->where('name', '생산 관리')
->first();
echo 'parent_id: ' . \\\$parent->id;
// 2. 현재 최대 sort_order 확인
\\\$maxSort = App\\\\Models\\\\Commons\\\\Menu::withoutGlobalScopes()
->where('parent_id', \\\$parent->id)
->max('sort_order') ?? 0;
// 3. 절곡품 관리 그룹 메뉴 추가 (폴더)
\\\$bending = App\\\\Models\\\\Commons\\\\Menu::create([
'tenant_id' => 1,
'parent_id' => \\\$parent->id,
'name' => '절곡품 관리',
'url' => null,
'icon' => 'tools',
'sort_order' => \\\$maxSort + 1,
'is_active' => true,
'options' => ['section' => 'main'],
]);
echo 'bending group id: ' . \\\$bending->id;
// 4. 하위 메뉴 추가
App\\\\Models\\\\Commons\\\\Menu::create([
'tenant_id' => 1,
'parent_id' => \\\$bending->id,
'name' => '기초관리',
'url' => '/bending/base',
'icon' => 'database',
'sort_order' => 1,
'is_active' => true,
'options' => ['section' => 'main', 'route_name' => 'bending.base.index'],
]);
App\\\\Models\\\\Commons\\\\Menu::create([
'tenant_id' => 1,
'parent_id' => \\\$bending->id,
'name' => '절곡품',
'url' => '/bending/products',
'icon' => 'stack',
'sort_order' => 2,
'is_active' => true,
'options' => ['section' => 'main', 'route_name' => 'bending.products.index'],
]);
echo 'Done!';
\""
```
#### 확인용 SQL (phpMyAdmin)
```sql
-- 생산관리 하위 메뉴 확인
SELECT id, parent_id, name, url, sort_order, is_active
FROM menus
WHERE tenant_id = 1
AND parent_id = (SELECT id FROM menus WHERE name = '생산 관리' AND tenant_id = 1 LIMIT 1)
ORDER BY sort_order;
```
---
## 2. 라우트
```php
// routes/web.php
// 파일 뷰어 (R2 이미지 스트리밍 — MNG 세션 인증)
Route::get('/files/{id}/view', [FileViewController::class, 'show'])->name('files.view');
Route::prefix('bending')->name('bending.')->group(function () {
// 기초관리
Route::get('/base', [BendingBaseController::class, 'index'])->name('base.index');
Route::get('/base/create', [BendingBaseController::class, 'create'])->name('base.create');
Route::post('/base', [BendingBaseController::class, 'store'])->name('base.store');
Route::get('/base/{id}', [BendingBaseController::class, 'show'])->name('base.show');
Route::get('/base/{id}/edit', [BendingBaseController::class, 'edit'])->name('base.edit');
Route::put('/base/{id}', [BendingBaseController::class, 'update'])->name('base.update');
Route::delete('/base/{id}', [BendingBaseController::class, 'destroy'])->name('base.destroy');
// 절곡품 (모델)
Route::get('/products', [BendingProductController::class, 'index'])->name('products.index');
Route::get('/products/create', [BendingProductController::class, 'create'])->name('products.create');
Route::post('/products', [BendingProductController::class, 'store'])->name('products.store');
Route::get('/products/{id}', [BendingProductController::class, 'show'])->name('products.show');
Route::get('/products/{id}/edit', [BendingProductController::class, 'edit'])->name('products.edit');
Route::put('/products/{id}', [BendingProductController::class, 'update'])->name('products.update');
Route::delete('/products/{id}', [BendingProductController::class, 'destroy'])->name('products.destroy');
});
```
### 파일 뷰어 (R2 이미지 프록시)
MNG는 Blade(서버사이드)이므로 `<img src="/api/v1/files/{id}/view">`로 직접 호출 시 sanctum 인증 문제 발생.
MNG 세션 인증으로 R2 파일을 스트리밍하는 프록시 라우트 필요.
```php
// FileViewController.php
class FileViewController extends Controller
{
public function show(int $id)
{
$file = File::findOrFail($id);
$stream = Storage::disk('r2')->readStream($file->file_path);
return response()->stream(function () use ($stream) {
fpassthru($stream);
if (is_resource($stream)) fclose($stream);
}, 200, [
'Content-Type' => $file->mime_type,
'Content-Disposition' => 'inline',
'Cache-Control' => 'private, max-age=3600',
]);
}
}
```
**Blade에서 사용**:
```html
<!-- 전개도 이미지 표시 -->
<img src="{{ route('files.view', $file->id) }}" alt="전개도">
<!-- 이미지 없을 때 fallback -->
@if($fileId)
<img src="{{ route('files.view', $fileId) }}" alt="전개도" class="max-w-full rounded">
@else
<div class="text-gray-400 text-center py-8">이미지 없음</div>
@endif
```
---
## 3. 화면 구성
### 3-1. 기초관리 목록 (`/bending/base`)
**프로토타입 참고**: `work/절곡/base.html`
```
┌─────────────────────────────────────────────────────────┐
│ 절곡 바라시 기초자료 [+ 신규 등록] │
├─────────────────────────────────────────────────────────┤
│ 필터: │
│ [전체|스크린|철재] [전체|인정|비인정] [그룹▼] [품명▼] [검색] │
├─────────────────────────────────────────────────────────┤
│ NO│등록일│대분류│인정│절곡물분류│품명│규격│이미지│재질│ │
│ │ │ │ │ │ │ │ │ │... │
├─────────────────────────────────────────────────────────┤
│ 265건 (1~15) [< 1 2 3 ... >] │
└─────────────────────────────────────────────────────────┘
```
**테이블 컬럼**: NO, 등록일, 대분류, 인정, 절곡물분류, 품명, 규격, 이미지, 재질, 폭합계, 절곡횟수, 역방향, A각, 폭합, 작성, 검색어, 비고, 작업
**HTMX 인터랙션**:
- 필터 토글 → `hx-get="/bending/base"` → 테이블 교체
- 검색 입력 → `hx-trigger="keyup changed delay:300ms"`
- 행 클릭 → 상세 페이지 이동
### 3-2. 기초관리 등록/수정 (`/bending/base/create`, `/bending/base/{id}/edit`)
**프로토타입 참고**: `work/절곡/base-form.html`
```
┌───────────────────────────────────┬──────────────────┐
│ [기본 정보] │ [형상 이미지] │
│ 등록일 | 대분류 | 인정 │ 이미지 업로드 │
│ 그룹 | 품명 | 재질 │ 이미지 미리보기 │
│ 폭합 | 규격 | 작성자 | 비고 │ 품목검색어 │
├───────────────────────────────────┤ │
│ [케이스 전용] (그룹=케이스 시) │ │
│ 점검구방향 | 너비 | 높이 | 전면밑 | 레일폭 │
├───────────────────────────────────┤ │
│ [절곡 입력 테이블] ★핵심 │ │
│ 번호 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ │
│ 입력 │ │ │ │ │ │ │ │
│ 연신율│ │ │ │ │ │ │ │
│ 연신율후│ │ │ │ │ │ │ │
│ 합계 │ │ │ │ │ │ │ │
│ 음영 │☐ │☐ │☐ │☐ │☐ │☐ │ │
│ A각 │☐ │☐ │☐ │☐ │☐ │☐ │ │
│ [비우기] [열추가] [열삭제] │ │
├───────────────────────────────────┤ │
│ [재질별 폭합] │ │
│ 재질 | 폭합계 │ │
└───────────────────────────────────┴──────────────────┘
```
**JS 동작 (필수)**:
- 입력 시 합계 자동계산
- 연신율 입력 시 연신율후 자동계산: rate="-1" → input-1mm, rate="1" → input+1mm, rate="" → input 그대로 (절곡 1회당 고정 1mm 보정)
- 열 추가/삭제 동적 DOM
- 그룹 변경 시 케이스 전용 필드 토글
- 폭합 필드 자동 업데이트
- 조회 모드: 입력 비활성화
### 3-3. 절곡품 목록 (`/bending/products`)
**프로토타입 참고**: `work/절곡/products.html`
```
┌─────────────────────────────────────────────────────────┐
│ 절곡품 관리 [+ 신규 등록] │
├─────────────────────────────────────────────────────────┤
│ [가이드레일 20] [케이스 30] [하단마감재 11] │
├─────────────────────────────────────────────────────────┤
│ 필터: (탭별 다른 필터) │
│ 가이드레일: [대분류] [인정] [모델▼] [검색] │
│ 케이스: [대분류] [인정] [점검구형태] [검색] │
├─────────────────────────────────────────────────────────┤
│ (탭별 다른 테이블 컬럼) │
└─────────────────────────────────────────────────────────┘
```
**탭별 컬럼**:
- 가이드레일: 번호, 등록일, 대분류, 인정, 제품코드, 검색어, 가로X세로, 형상, 마감, 소요자재량, 형태, 작성, 비고
- 케이스: 번호, 등록일, 박스(가로X세로), 점검구형태, 전면부밑면, 레일폭, 소요자재량, 검색어, 형태, 작성, 비고
- 하단마감재: 번호, 등록일, 대분류, 인정, 제품코드, 가로X세로, 검색어, 마감형태, 소요자재량, 형태, 작성, 비고
### 3-4. 절곡품 등록/수정 (`/bending/products/create`, `/bending/products/{id}/edit`)
**프로토타입 참고**: `work/절곡/product-form.html`
타입별로 폼 헤더가 다름 — 아래 3가지 구분:
#### 가이드레일 폼
```
┌───────────────────────────────────┬──────────────────┐
│ [기본 정보] │ [형상 이미지] │
│ 등록일 | 작성자 | 비고 │ 이미지 업로드 │
├───────────────────────────────────┤ │
│ [가이드레일 정보] │ 품목검색어 │
│ 가로(폭) × 세로(높이) │ │
│ 대분류: ○스크린 ○철재 │ │
│ 인정: ○인정 ○비인정 │ │
│ 모델: [KSS01 ▼] │ │
│ 마감: [SUS마감 ▼] │ │
│ 형상: [벽면형 ▼] │ │
├───────────────────────────────────┤ │
│ [절곡 입력] ★핵심 │ │
│ 파트 탭: [본체상부] [본체하부] [마감재] │
│ (파트별 절곡 테이블) │
├───────────────────────────────────┤ │
│ [재질별 폭합] │ │
│ 재질 | 폭합계 │ │
└───────────────────────────────────┴──────────────────┘
```
#### 케이스 폼
```
┌───────────────────────────────────┬──────────────────┐
│ [기본 정보] │ [형상 이미지] │
│ 등록일 | 작성자 │ 이미지 업로드 │
├───────────────────────────────────┤ │
│ [케이스 정보] │ 품목검색어 │
│ 가로(폭) × 세로(높이) │ 비고 │
│ 전면밑: [50] | 레일폭: [75] │ │
│ 점검구: ○양면 ○밑면 ○후면 │ │
├───────────────────────────────────┤ │
│ [절곡 입력] ★핵심 │ │
│ 파트 탭: [상부덮개] [전면] [점검구] [린텔] [후면코너] │
│ (파트별 절곡 테이블) │
├───────────────────────────────────┤ │
│ [재질별 폭합] │ │
│ EGI 1.55T | 2652 │ │
└───────────────────────────────────┴──────────────────┘
```
※ 케이스는 대분류/인정/모델/마감 필드 **없음** — 규격+점검구형태로만 구분
#### 하단마감재 폼
```
┌───────────────────────────────────┬──────────────────┐
│ [기본 정보] │ [형상 이미지] │
│ 등록일 | 작성자 | 비고 │ 이미지 업로드 │
├───────────────────────────────────┤ │
│ [하단마감재 정보] │ 품목검색어 │
│ 가로(폭) × 세로(높이) │ │
│ 대분류: ○스크린 ○철재 │ │
│ 인정: ○인정 ○비인정 │ │
│ 모델: [KSS01 ▼] │ │
│ 마감: [SUS마감 ▼] │ │
│ (형상 필드 없음) │ │
├───────────────────────────────────┤ │
│ [절곡 입력] ★핵심 │ │
│ 파트 1개 (하단마감재 단일) │
├───────────────────────────────────┤ │
│ [재질별 폭합] │ │
│ 재질 | 폭합계 │ │
└───────────────────────────────────┴──────────────────┘
```
**타입별 폼 차이 요약**:
| 필드 | 가이드레일 | 케이스 | 하단마감재 |
|------|-----------|--------|----------|
| 등록일/작성자/비고 | ✅ | ✅ | ✅ |
| 가로×세로 | ✅ | ✅ | ✅ |
| 대분류 (스크린/철재) | ✅ | ❌ | ✅ |
| 인정/비인정 | ✅ | ❌ | ✅ |
| 모델 | ✅ | ❌ | ✅ |
| 마감 (SUS/EGI) | ✅ | ❌ | ✅ |
| 형상 (벽면/측면) | ✅ | ❌ | ❌ |
| 전면밑/레일폭 | ❌ | ✅ | ❌ |
| 점검구 형태 | ❌ | ✅ | ❌ |
| 파트 수 | 3~5 | 5 | 1 |
| 품목검색어 | ✅ | ✅ | ✅ |
| 재질별 폭합 | ✅ | ✅ | ✅ |
**파트 구성**:
- 가이드레일: 3~5파트 (본체 상부, 본체 하부, 마감재, ...)
- 케이스: 5파트 (상부덮개, 전면, 점검구, 린텔, 후면코너)
- 하단마감재: 1파트
---
## 4. Blade 파일 구조
```
resources/views/bending/
├─ base/
│ ├─ index.blade.php ← 기초관리 목록
│ ├─ form.blade.php ← 등록/수정/조회 (mode 분기)
│ └─ partials/
│ ├─ table.blade.php ← HTMX 갱신 대상
│ ├─ filters.blade.php ← 필터 영역
│ └─ bend-table.blade.php ← 절곡 입력 테이블 (재사용)
├─ products/
│ ├─ index.blade.php ← 절곡품 탭 목록
│ ├─ form.blade.php ← 등록/수정
│ └─ partials/
│ ├─ tab-guiderail.blade.php ← 가이드레일 탭 테이블
│ ├─ tab-case.blade.php ← 케이스 탭 테이블
│ ├─ tab-bottom.blade.php ← 하단마감재 탭 테이블
│ └─ filters-*.blade.php ← 탭별 필터
└─ components/
└─ bend-input-table.blade.php ← 절곡 입력 테이블 공용 컴포넌트
```
---
## 5. 기존 MNG 패턴 준수
| 항목 | 기존 패턴 | 적용 |
|------|----------|------|
| 레이아웃 | `layouts/app.blade.php` 상속 | `@extends('layouts.app')` |
| 사이드바 | `partials/sidebar.blade.php` (동적 DB 메뉴) | tinker로 `menus` 테이블에 추가 |
| HTMX | 기존 페이지 패턴 참고 | `hx-get`, `hx-target`, `hx-trigger` |
| Tailwind | 기존 클래스 패턴 | 동일 스타일 사용 |
| 테이블 | 기존 목록 페이지 참고 | 정렬/페이지네이션 동일 |
---
## 6. 주의사항
### 아키텍처
-**MNG는 샘플 확인용** — 실제 운영 화면은 React
-**MNG/React 모두 동일한 API 엔드포인트 호출** (`/api/v1/bending-items`, `/api/v1/guiderail-models`)
- ✅ MNG에서 API 연동 검증 후 React 화면 구현으로 진행
- ❌ MNG에서 Eloquent 직접 DB 접근 금지 — 반드시 API 통해 접근
### 메뉴/사이드바
- ⚠️ 메뉴 시더 실행 금지
- ⚠️ sidebar-static.blade.php 사용 안 함 — 동적 메뉴(DB `menus` 테이블) 사용
- ✅ tinker로 `menus` 테이블에 직접 추가
### 기존 코드 보호
- ⚠️ 기존 bending-worklog.blade.php 무변경
- ⚠️ 기존 bending-inspection-data.blade.php 무변경
- ⚠️ BendingInfoBuilder / PrefixResolver 무변경
---
## 7. 형상 이미지 구현 전략 (단계별)
### 1차: 이미지 업로드만
MNG는 샘플 확인용이므로 1차에서는 **파일 업로드 + 미리보기**만 구현.
```
┌──────────────────┐
│ [형상 이미지] │
│ │
│ ┌────────────┐ │
│ │ 미리보기 │ │
│ │ (없으면 │ │
│ │ placeholder)│ │
│ └────────────┘ │
│ │
│ [파일 선택] │ ← input[type=file] accept="image/*"
│ [Ctrl+V 붙여넣기]│ ← 클립보드 이미지 지원
│ 품목검색어: [___] │
└──────────────────┘
```
**구현 범위**:
| 기능 | 1차 | 2차 |
|------|-----|-----|
| 파일 업로드 (`input[type=file]`) | ✅ | ✅ |
| 이미지 미리보기 | ✅ | ✅ |
| Ctrl+V 클립보드 붙여넣기 | ✅ | ✅ |
| R2 저장 (API files 엔드포인트) | ✅ | ✅ |
| 기존 이미지 표시 (`/files/{id}/view`) | ✅ | ✅ |
| Canvas 그리기 도구 | ❌ | ✅ |
| 호버 시 확대 팝업 | ❌ | ✅ |
**1차 업로드 흐름**:
```
[파일 선택] or [Ctrl+V]
→ 미리보기 표시 (FileReader → img.src)
→ 폼 저장 시 FormData로 API 전송
→ API가 R2에 저장 → file_id 반환
→ bending_base_data.image_file_id에 저장
```
**Blade 이미지 업로드 컴포넌트**:
```html
<!-- 1차: 단순 업로드 + 미리보기 -->
<div class="border-2 border-dashed border-gray-300 rounded-lg p-4">
@if($imageFileId)
<img src="{{ route('files.view', $imageFileId) }}"
class="max-w-full rounded mb-2" alt="전개도">
@else
<div class="text-gray-400 text-center py-8">이미지 없음</div>
@endif
<input type="file" name="image" accept="image/*"
onchange="previewImage(this)" class="mt-2">
<img id="image-preview" class="hidden max-w-full rounded mt-2">
</div>
```
**클립보드 붙여넣기 JS**:
```javascript
document.addEventListener('paste', function(e) {
const items = e.clipboardData?.items;
if (!items) return;
for (const item of items) {
if (item.type.startsWith('image/')) {
const file = item.getAsFile();
const dt = new DataTransfer();
dt.items.add(file);
document.querySelector('input[name="image"]').files = dt.files;
previewImage(document.querySelector('input[name="image"]'));
break;
}
}
});
function previewImage(input) {
const file = input.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = e => {
const preview = document.getElementById('image-preview');
preview.src = e.target.result;
preview.classList.remove('hidden');
};
reader.readAsDataURL(file);
}
```
### 2차: Canvas 그리기 도구 추가 (React 화면과 함께)
레거시 `5130/js/imageEditor.js` (Fabric.js 기반, 511줄) 기반으로 Canvas 에디터 통합.
React 화면 구현 시 함께 진행 — MNG에는 필요 시에만 백포트.
**레거시 Canvas 에디터 파일 위치**:
| 파일 | 위치 | 용도 |
|------|------|------|
| `imageEditor.js` | `5130/js/imageEditor.js` | Fabric.js Canvas 에디터 (511줄) |
| `drawLib.js` | `5130/js/drawLib.js` | Pure Canvas 대안 (272줄) |
| `drawingModule.js` | `5130/js/drawingModule.js` | 독립 모달 포함 (966줄) |
| `imageHandler.js` | `5130/guiderail/js/imageHandler.js` | 이미지 검색/호버 팝업 |
**2차 추가 기능**:
- [그리기] 버튼 → Canvas 모달 (Poly/Free/Line/Text/Eraser)
- 직각 고정 모드
- 그린 이미지 → Base64 → API 저장
- 목록에서 이미지 호버 시 확대 팝업

View File

@@ -0,0 +1,89 @@
# Step 4: React 견적 화면 이미지 연동
> **프로젝트**: React (`sam/react`)
> **선행 조건**: Step 2 (API), Step 3 (MNG에서 데이터 등록 후)
> **참조**: 기존 GuideRailSection 컴포넌트
---
## 1. 목적
견적 페이지(`/sales/quote-management/new`)에서 가이드레일 모델 선택 시
전개도 이미지 + 부품 조합 테이블을 표시한다.
---
## 2. 현재 흐름
```
제품 선택 → BOM 계산 (BendingInfoBuilder)
→ product_code, finish_material 확정
→ 가이드레일 모델 결정
→ 텍스트만 표시 ❌ 이미지 없음
```
## 3. 목표 흐름
```
제품 선택 → BOM 계산 (기존 그대로)
→ 모델 확정
→ GET /api/guiderail-models/{id} 호출 🆕
→ GuiderailPreview 컴포넌트 렌더링 🆕
├─ 전개도 이미지
└─ 부품 조합 테이블 (부품명/재질/수량/전개폭)
```
---
## 4. 구현 사항
### 4-1. GuiderailPreview 컴포넌트
```
┌─────────────────────────────────────────────────────┐
│ 가이드레일: KSS01 벽면형 | 인정 | SUS마감 | 70×120 │
├──────────────────────┬──────────────────────────────┤
│ 전개도 이미지 │ 부품 조합 │
│ ┌────────────────┐ │ # │ 부품 │ 재질 │ 수량 │
│ │ │ │ 1 │ 마감재 │ SUS │ 2 │
│ │ (이미지) │ │ 2 │ 본체 │ EGI │ 1 │
│ │ │ │ 3 │ C형 │ EGI │ 1 │
│ └────────────────┘ │ 4 │ D형 │ EGI │ 1 │
└──────────────────────┴──────────────────────────────┘
```
### 4-2. 삽입 위치
견적 페이지에서 BOM 결과 표시 영역 하단 (기존 레이아웃 무변경)
### 4-3. 데이터 흐름
```
BOM 계산 결과 → product_code + finish_material
→ API 호출: GET /api/guiderail-models?model={code}&check_type={형상}
→ 응답: image_url + components + material_summary
→ GuiderailPreview 렌더링
```
---
## 5. 주의사항
- 기존 견적 계산 로직 무변경
- 기존 GuideRailSection (작업지시서용) 무변경 — 별도 컴포넌트
- 이미지 없는 모델: 텍스트만 표시 (graceful degradation)
- 모바일 반응형 처리
---
## 6. 범위 (추후 확정)
| 영역 | 설명 | 시점 |
|------|------|------|
| 견적 이미지 연동 | GuiderailPreview 컴포넌트 | Step 1~3 완료 후 |
| 절곡품 관리 화면 | React 버전 CRUD (MNG 대체) | MNG 샘플 검증 후 |
- MNG는 **샘플 확인용** — API 연동 검증이 목적
- **실제 운영 화면은 React**에서 구현 (MNG와 동일한 API 호출)
- React 화면 상세 설계는 MNG 검증 후 별도 문서로 작성 예정
- 현재 문서는 견적 이미지 연동 범위만 정의