- step1: 데이터 임포트 완료 (170+60건), artisan 커맨드 7개 실행 결과 - step2: API 12개 엔드포인트 완료, item_category 필수 필터 추가 - step3: MNG 샘플 완료 (4개 메뉴, 이미지 473건) - step4: React 구현 가이드 전면 작성 (API 응답 구조, 컴포넌트 설계, 실무 노트) - 코드 체계 변경 불가 사유, 265vs170 차이 설명, 운영 전 정리 항목 추가
24 KiB
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 저장
- 목록에서 이미지 호버 시 확대 팝업