Files
sam-docs/dev/dev_plans/bending-management/step3-MNG화면.md
강영보 fbd0510cc1 docs: [bending] 절곡품 관리 개발 완료 상태 업데이트
- step1: 데이터 임포트 완료 (170+60건), artisan 커맨드 7개 실행 결과
- step2: API 12개 엔드포인트 완료, item_category 필수 필터 추가
- step3: MNG 샘플 완료 (4개 메뉴, 이미지 473건)
- step4: React 구현 가이드 전면 작성 (API 응답 구조, 컴포넌트 설계, 실무 노트)
- 코드 체계 변경 불가 사유, 265vs170 차이 설명, 운영 전 정리 항목 추가
2026-03-17 11:33:47 +09:00

24 KiB
Raw Blame History

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로 메뉴 추가 (서버에서 실행)

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)

-- 생산관리 하위 메뉴 확인
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. 라우트

// 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 파일을 스트리밍하는 프록시 라우트 필요.

// 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에서 사용:

<!-- 전개도 이미지 표시 -->
<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 이미지 업로드 컴포넌트:

<!-- 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:

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 저장
  • 목록에서 이미지 호버 시 확대 팝업