# Step 3: MNG 관리 화면 (Blade + HTMX) ✅ 완료

> **프로젝트**: MNG (`sam/mng`)
> **선행 조건**: Step 2 (API 엔드포인트) 완료
> **상태**: ✅ 샘플 구현 완료 (2026-03-16~17)
> **참조**: 프로토타입 `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 저장
- 목록에서 이미지 호버 시 확대 팝업
