# MNG 품목관리 페이지 계획 > **작성일**: 2026-02-19 > **목적**: MNG 관리자 패널에 3-Panel 품목관리 페이지 추가 (좌측 리스트 + 중앙 BOM 트리 + 우측 상세) > **기준 문서**: docs/rules/item-policy.md, docs/specs/item-master-integration.md > **상태**: ✅ 기본 구현 완료 (미커밋) → Phase 3 수식 연동은 별도 계획 --- ## 📍 현재 진행 상태 | 항목 | 내용 | |------|------| | **마지막 완료 작업** | Phase 1~2 전체 구현 완료 (미커밋 상태) | | **다음 작업** | 수식 엔진 연동 → `docs/plans/mng-item-formula-integration-plan.md` 참조 | | **진행률** | 12/12 (100%) - 기본 3-Panel 구현 완료 | | **마지막 업데이트** | 2026-02-19 | | **후속 작업** | FormulaEvaluatorService 연동 (별도 계획 문서) | --- ## 1. 개요 ### 1.1 배경 MNG 관리자 패널에 품목(Items)을 관리하고 BOM 연결관계를 시각적으로 파악할 수 있는 페이지가 필요하다. 현재 items 테이블은 products + materials 통합 구조로, `items.bom` JSON 필드에 BOM 구성을 저장한다. ### 1.2 기준 원칙 ``` ┌─────────────────────────────────────────────────────────────────┐ │ 🎯 핵심 원칙 │ ├─────────────────────────────────────────────────────────────────┤ │ - MNG에서 마이그레이션 파일 생성 금지 (API에서만) │ │ - Service-First (비즈니스 로직은 Service 클래스에만) │ │ - FormRequest 필수 (Controller 검증 금지) │ │ - BelongsToTenant (테넌트 격리) │ │ - Blade + HTMX + Tailwind (Alpine.js 미사용) │ │ - 세션 기반 테넌트 필터링: session('selected_tenant_id') │ └─────────────────────────────────────────────────────────────────┘ ``` ### 1.3 변경 승인 정책 | 분류 | 예시 | 승인 | |------|------|------| | ✅ 즉시 가능 | 모델/서비스/뷰/컨트롤러/라우트 생성 | 불필요 | | ⚠️ 컨펌 필요 | 기존 라우트 수정, 사이드바 메뉴 추가 | **필수** | | 🔴 금지 | mng에서 마이그레이션 생성, 테이블 구조 변경 | 별도 협의 | ### 1.4 MNG 절대 금지 규칙 (인라인) ``` ❌ mng/database/migrations/ 에 파일 생성 금지 ❌ docker exec sam-mng-1 php artisan migrate 실행 금지 ❌ php artisan db:seed --class=*MenuSeeder 실행 금지 ❌ 메뉴 시더 파일 생성/실행 금지 (부서별 권한 초기화됨) ❌ Controller에서 직접 DB 쿼리 금지 (Service-First) ❌ Controller에서 직접 validate() 금지 (FormRequest 필수) ``` --- ## 2. 기능 설계 ### 2.1 3-Panel 레이아웃 ``` ┌─────────────────────────────────────────────────────────────────────┐ │ Header (64px) - 테넌트 선택 (session 기반 필터링) │ ├──────────┬─────────────────────────────┬────────────────────────────┤ │ 좌측 │ 중앙 │ 우측 │ │ (280px) │ (flex-1) │ (380px) │ │ │ │ │ │ [검색] │ │ ┌──────────────────────┐ │ │ ________│ │ │ 기본정보 │ │ │ │ BOM 재귀 트리 │ │ 코드: P-001 │ │ │ 품목 1 ◀│ ┌ 완제품A │ │ 이름: 스크린 제품 │ │ │ 품목 2 │ ├─ 부품B │ │ 유형: FG │ │ │ 품목 3 │ │ ├─ 원자재C │ │ 단위: EA │ │ │ 품목 4 │ │ └─ 부자재D │ │ 카테고리: ... │ │ │ 품목 5 │ ├─ 부품E │ ├──────────────────────┤ │ │ ... │ │ ├─ 원자재F │ │ BOM 구성 (1depth) │ │ │ │ │ └─ 소모품G │ │ - 부품B (2ea) │ │ │ │ └─ 원자재H │ │ - 부품E (1ea) │ │ │ │ │ │ - 원자재H (0.5kg) │ │ │ │ ← 전체 재귀 트리 → │ ├──────────────────────┤ │ │ │ (좌측 선택 품목 기준) │ │ 절곡 정보 │ │ │ │ │ │ (bending_details) │ │ │ │ │ ├──────────────────────┤ │ │ │ │ │ 이미지/파일 │ │ │ │ │ │ 📎 도면.pdf │ │ │ │ │ │ 📎 인증서.pdf │ │ │ │ │ └──────────────────────┘ │ ├──────────┴─────────────────────────────┴────────────────────────────┤ │ ← 클릭 시 어디서든 → 우측 상세 갱신 │ └─────────────────────────────────────────────────────────────────────┘ ``` ### 2.2 패널별 상세 동작 #### 좌측 패널 (품목 리스트) - **상단 검색**: `` debounce 300ms, 코드+이름 동시 검색 - **리스트**: 스크롤 가능, 선택된 항목 하이라이트 - **표시 정보**: 품목코드, 품목명, 유형(FG/PT/SM/RM/CS) 뱃지 - **테넌트 필터**: 헤더에서 선택된 테넌트 자동 적용 (BelongsToTenant) - **클릭 시**: 중앙 트리 갱신 + 우측 상세 갱신 #### 중앙 패널 (BOM 재귀 트리) - **데이터 소스**: `items.bom` JSON → child_item_id 재귀 탐색 - **트리 깊이**: 전체 재귀 (BOM → BOM → BOM ...) - **노드 표시**: 품목코드 + 품목명 + 수량 + 유형 뱃지 - **펼침/접힘**: 노드별 토글 가능 - **클릭 시**: 해당 품목으로 우측 상세 갱신 (좌측 선택은 변경 안 함) #### 우측 패널 (선택 품목 상세) - **기본정보**: 코드, 이름, 유형, 단위, 카테고리, 활성 여부, options - **BOM 구성 (1depth)**: 직접 연결된 자식 품목만 (재귀 X) - **절곡 정보**: item_details.bending_details JSON (해당 시) - **파일/이미지**: 연결된 files 목록 - **scope**: 선택된 품목에 직접 연결된 정보만 (1depth) ### 2.3 데이터 흐름 ``` [좌측 검색/선택] │ ├──→ HTMX GET /api/admin/items?search=xxx │ → 좌측 리스트 갱신 │ ├──→ fetch GET /api/admin/items/{id}/bom-tree │ → 중앙 트리 갱신 (재귀 JSON 반환 → Vanilla JS 렌더링) │ └──→ HTMX GET /api/admin/items/{id}/detail → 우측 상세 갱신 [중앙 트리 노드 클릭] │ └──→ HTMX GET /api/admin/items/{id}/detail → 우측 상세만 갱신 (중앙 트리 유지) ``` --- ## 3. 기술 설계 ### 3.1 DB 스키마 (기존 테이블 활용, 변경 없음) ```sql -- items (통합 품목) - 이미 존재하는 테이블 -- item_type: FG(완제품), PT(부품), SM(부자재), RM(원자재), CS(소모품) -- item_category: SCREEN, STEEL, BENDING, ALUMINUM 등 CREATE TABLE items ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, item_type VARCHAR(10) NOT NULL, -- FG/PT/SM/RM/CS item_category VARCHAR(50) NULL, -- SCREEN/STEEL/BENDING/ALUMINUM 등 code VARCHAR(50) NOT NULL, name VARCHAR(200) NOT NULL, unit VARCHAR(20) NULL, category_id BIGINT UNSIGNED NULL, -- FK → categories.id bom JSON NULL, -- [{child_item_id: 5, quantity: 2.5}, ...] attributes JSON NULL, -- 동적 필드 (migration 등에서 가져온 데이터) attributes_archive JSON NULL, -- 아카이브 options JSON NULL, -- {lot_managed, consumption_method, ...} description TEXT NULL, is_active TINYINT(1) DEFAULT 1, created_by BIGINT UNSIGNED NULL, updated_by BIGINT UNSIGNED NULL, deleted_at TIMESTAMP NULL, created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL, INDEX (tenant_id), INDEX (item_type), INDEX (code), INDEX (category_id) ); -- item_details (1:1 확장) - 이미 존재하는 테이블 CREATE TABLE item_details ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, item_id BIGINT UNSIGNED NOT NULL UNIQUE, -- FK → items.id (1:1) -- Products 전용 is_sellable TINYINT(1) DEFAULT 0, is_purchasable TINYINT(1) DEFAULT 0, is_producible TINYINT(1) DEFAULT 0, safety_stock DECIMAL(10,2) NULL, lead_time INT NULL, is_variable_size TINYINT(1) DEFAULT 0, product_category VARCHAR(50) NULL, part_type VARCHAR(50) NULL, bending_diagram VARCHAR(255) NULL, -- 절곡 도면 파일 경로 bending_details JSON NULL, -- 절곡 상세 정보 JSON specification_file VARCHAR(255) NULL, specification_file_name VARCHAR(255) NULL, certification_file VARCHAR(255) NULL, certification_file_name VARCHAR(255) NULL, certification_number VARCHAR(100) NULL, certification_start_date DATE NULL, certification_end_date DATE NULL, -- Materials 전용 is_inspection CHAR(1) NULL, -- 'Y'/'N' item_name VARCHAR(200) NULL, specification VARCHAR(500) NULL, search_tag VARCHAR(500) NULL, remarks TEXT NULL, created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL ); -- files (폴리모픽) - 이미 존재하는 테이블 -- 품목 파일: document_id = items.id, document_type = '1' (ITEM_GROUP_ID) CREATE TABLE files ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NULL, document_id BIGINT UNSIGNED NOT NULL, -- 연결 대상 ID (items.id) document_type VARCHAR(10) NOT NULL, -- '1' = ITEM_GROUP_ID original_name VARCHAR(255) NOT NULL, stored_name VARCHAR(255) NOT NULL, path VARCHAR(500) NOT NULL, mime_type VARCHAR(100) NULL, size BIGINT UNSIGNED NULL, created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL ); -- categories - 이미 존재하는 테이블 -- 품목 카테고리 (code_group으로 구분, 계층 구조) CREATE TABLE categories ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, parent_id BIGINT UNSIGNED NULL, -- 자기 참조 (트리) code_group VARCHAR(50) NOT NULL, -- 카테고리 그룹 profile_code VARCHAR(50) NULL, code VARCHAR(50) NOT NULL, name VARCHAR(200) NOT NULL, is_active TINYINT(1) DEFAULT 1, sort_order INT DEFAULT 0, description TEXT NULL, created_by BIGINT UNSIGNED NULL, updated_by BIGINT UNSIGNED NULL, deleted_by BIGINT UNSIGNED NULL, deleted_at TIMESTAMP NULL, created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL ); ``` ### 3.2 BOM 트리 재귀 로직 ```php // ItemManagementService::getBomTree(int $itemId, int $maxDepth = 10): array public function getBomTree(int $itemId, int $maxDepth = 10): array { $item = Item::with('details')->findOrFail($itemId); return $this->buildBomNode($item, 0, $maxDepth, []); } private function buildBomNode(Item $item, int $depth, int $maxDepth, array $visited): array { // 순환 참조 방지: visited 배열 + maxDepth 이중 안전장치 if (in_array($item->id, $visited) || $depth >= $maxDepth) { return $this->formatNode($item, $depth, []); } $visited[] = $item->id; $children = []; $bomData = $item->bom ?? []; if (!empty($bomData)) { $childIds = array_column($bomData, 'child_item_id'); $childItems = Item::whereIn('id', $childIds)->get()->keyBy('id'); foreach ($bomData as $bom) { $childItem = $childItems->get($bom['child_item_id']); if ($childItem) { $childNode = $this->buildBomNode($childItem, $depth + 1, $maxDepth, $visited); $childNode['quantity'] = $bom['quantity'] ?? 1; $children[] = $childNode; } } } return $this->formatNode($item, $depth, $children); } private function formatNode(Item $item, int $depth, array $children): array { return [ 'id' => $item->id, 'code' => $item->code, 'name' => $item->name, 'item_type' => $item->item_type, 'unit' => $item->unit, 'depth' => $depth, 'has_children' => count($children) > 0, 'children' => $children, ]; } ``` ### 3.3 API 엔드포인트 설계 | Method | Endpoint | 설명 | 반환 | |--------|----------|------|------| | GET | `/api/admin/items` | 품목 목록 (검색, 페이지네이션) | HTML partial | | GET | `/api/admin/items/{id}/bom-tree` | BOM 재귀 트리 | JSON | | GET | `/api/admin/items/{id}/detail` | 품목 상세 (1depth BOM, 파일, 절곡) | HTML partial | #### GET /api/admin/items | 파라미터 | 타입 | 설명 | |---------|------|------| | search | string | 코드+이름 검색 (LIKE) | | item_type | string | 유형 필터 (FG,PT,SM,RM,CS 쉼표 구분) | | per_page | int | 페이지 크기 (default: 50) | | page | int | 페이지 번호 | #### GET /api/admin/items/{id}/bom-tree | 파라미터 | 타입 | 설명 | |---------|------|------| | max_depth | int | 최대 재귀 깊이 (default: 10) | **응답 (JSON)**: ```json { "id": 1, "code": "SCREEN-001", "name": "스크린 제품", "item_type": "FG", "unit": "EA", "depth": 0, "has_children": true, "children": [ { "id": 5, "code": "SLAT-001", "name": "슬랫", "item_type": "PT", "quantity": 2.5, "depth": 1, "has_children": true, "children": [ { "id": 12, "code": "STEEL-001", "name": "강판", "item_type": "RM", "quantity": 1.0, "depth": 2, "has_children": false, "children": [] } ] } ] } ``` #### GET /api/admin/items/{id}/detail **응답 (HTML partial)**: 기본정보 + BOM 1depth + 절곡정보 + 파일 목록 ### 3.4 파일 구조 ``` mng/ ├── app/ │ ├── Http/ │ │ └── Controllers/ │ │ ├── ItemManagementController.php # Web (Blade 화면) │ │ └── Api/Admin/ │ │ └── ItemManagementApiController.php # API (HTMX) │ ├── Models/ │ │ ├── Category.php # ⚠️ 이미 존재 (수정 불필요) │ │ └── Items/ │ │ ├── Item.php # ⚠️ 이미 존재 → 보완 필요 │ │ └── ItemDetail.php # 신규 생성 │ ├── Services/ │ │ └── ItemManagementService.php # BOM 트리, 검색, 상세 │ └── Traits/ │ └── BelongsToTenant.php # ⚠️ 이미 존재 (수정 불필요) ├── resources/ │ └── views/ │ └── item-management/ │ ├── index.blade.php # 메인 (3-Panel) │ └── partials/ │ ├── item-list.blade.php # 좌측 리스트 │ ├── bom-tree.blade.php # 중앙 트리 (JS 렌더링) │ └── item-detail.blade.php # 우측 상세 └── routes/ ├── web.php # + items 라우트 추가 └── api.php # + items API 라우트 추가 ``` ### 3.5 트리 렌더링 방식 **Vanilla JS + Tailwind (라이브러리 미사용)** - MNG 기존 패턴 유지 ```javascript // BOM 트리 JSON → HTML 변환 function renderBomTree(node, container) { const li = document.createElement('li'); li.className = 'ml-4'; // 노드 렌더링 const nodeEl = document.createElement('div'); nodeEl.className = 'flex items-center gap-2 py-1 px-2 rounded cursor-pointer hover:bg-blue-50'; nodeEl.onclick = () => selectTreeNode(node.id); // 펼침/접힘 토글 if (node.has_children) { const toggle = document.createElement('span'); toggle.className = 'text-gray-400 cursor-pointer'; toggle.textContent = '▶'; toggle.onclick = (e) => { e.stopPropagation(); toggleNode(toggle, childList); }; nodeEl.appendChild(toggle); } else { // 빈 공간 (들여쓰기 맞춤) const spacer = document.createElement('span'); spacer.className = 'w-4 inline-block'; nodeEl.appendChild(spacer); } // 유형 뱃지 + 코드 + 이름 + 수량 nodeEl.innerHTML += ` ${node.item_type} ${node.code} ${node.name} ${node.quantity ? `(${node.quantity})` : ''} `; li.appendChild(nodeEl); // 자식 노드 재귀 렌더링 if (node.children && node.children.length > 0) { const childList = document.createElement('ul'); childList.className = 'border-l border-gray-200'; node.children.forEach(child => renderBomTree(child, childList)); li.appendChild(childList); } container.appendChild(li); } // 트리 노드 펼침/접힘 function toggleNode(toggle, childList) { if (childList.style.display === 'none') { childList.style.display = ''; toggle.textContent = '▼'; } else { childList.style.display = 'none'; toggle.textContent = '▶'; } } ``` --- ## 4. 대상 범위 ### Phase 1: 백엔드 (모델 + 서비스 + API) | # | 작업 항목 | 상태 | 비고 | |---|----------|:----:|------| | 1.1 | Item 모델 보완 (mng/app/Models/Items/Item.php) | ✅ | BelongsToTenant, 관계, 스코프, 상수, 헬퍼 추가 | | 1.2 | ItemDetail 모델 생성 (mng/app/Models/Items/ItemDetail.php) | ✅ | 1:1 관계, is_variable_size 포함 | | 1.3 | ItemManagementService 생성 | ✅ | getItemList, getBomTree(재귀), getItemDetail | | 1.4 | ItemManagementApiController 생성 | ✅ | index(HTML), bomTree(JSON), detail(HTML) | | 1.5 | API 라우트 등록 (routes/api.php) | ✅ | /api/admin/items/* (3개 라우트) | | 1.6 | File 모델 생성 (mng/app/Models/Commons/File.php) | ✅ | Item.files() 관계용 | ### Phase 2: 프론트엔드 (Blade + JS) | # | 작업 항목 | 상태 | 비고 | |---|----------|:----:|------| | 2.1 | 메인 페이지 (index.blade.php) - 3-Panel 레이아웃 | ✅ | Tailwind flex, 3-Panel | | 2.2 | 좌측 패널 (item-list.blade.php) + 실시간 검색 | ✅ | HTMX + debounce 300ms + 유형 필터 | | 2.3 | 중앙 패널 (bom-tree.blade.php) + JS 트리 렌더링 | ✅ | Vanilla JS 재귀 렌더링 | | 2.4 | 우측 패널 (item-detail.blade.php) | ✅ | 기본정보+BOM 1depth+절곡+파일 | | 2.5 | ItemManagementController (Web) 생성 | ✅ | HX-Redirect 패턴 | | 2.6 | Web 라우트 등록 (routes/web.php) | ✅ | GET /item-management | | 2.7 | 유형별 뱃지 스타일 + 트리 라인 CSS | ✅ | Tailwind inline + JS getTypeBadgeClass | ### Phase 3: 수식 엔진 연동 (후속 작업) > 별도 계획 문서: `docs/plans/mng-item-formula-integration-plan.md` > > 가변사이즈 FG 품목 선택 시 오픈사이즈(W,H) 입력 → FormulaEvaluatorService 동적 산출 → 중앙 패널 탭 전환 표시 --- ## 5. 작업 절차 ### Step 1: 모델 보완/생성 (Phase 1.1, 1.2) ``` ├── mng/app/Models/Items/Item.php 보완 (기존 파일 존재) │ 현재 상태: SoftDeletes만 있음, BelongsToTenant 없음, 관계 없음 │ 추가 필요: │ - use App\Traits\BelongsToTenant 추가 │ - $fillable에 category_id, bom, attributes, options, description 추가 │ - $casts에 bom→array, options→array 추가 │ - 관계: details(), category(), files() │ - 스코프: type(), active(), search() │ - 상수: TYPE_FG 등, PRODUCT_TYPES, MATERIAL_TYPES │ - 헬퍼: isProduct(), isMaterial(), getBomChildIds() │ └── mng/app/Models/Items/ItemDetail.php 생성 (신규) - item() belongsTo 관계 - $fillable: 전체 필드 (섹션 A.3 참고) - $casts: bending_details→array, is_sellable→boolean 등 ``` ### Step 2: 서비스 생성 (Phase 1.3) ``` ├── mng/app/Services/ItemManagementService.php 생성 │ - getItemList(array $filters): LengthAwarePaginator │ └ Item::query()->search($search)->active()->orderBy('code')->paginate($perPage) │ - getBomTree(int $itemId, int $maxDepth = 10): array │ └ 재귀 buildBomNode() (섹션 3.2 코드) │ - getItemDetail(int $itemId): array │ └ Item::with(['details', 'category', 'files'])->findOrFail($id) │ └ BOM 1depth: items.bom JSON에서 child_item_id 추출 → Item::whereIn() │ └── 테넌트 스코프 자동 적용 (BelongsToTenant가 글로벌 스코프 등록) ``` ### Step 3: API 컨트롤러 + 라우트 (Phase 1.4, 1.5) ``` ├── mng/app/Http/Controllers/Api/Admin/ItemManagementApiController.php │ - __construct(private readonly ItemManagementService $service) │ - index(Request $request): View │ └ HTMX 요청 시 HTML partial 반환 (Blade view render) │ - bomTree(int $id): JsonResponse │ └ JSON 반환 (JS에서 트리 렌더링) │ - detail(int $id): View │ └ HTML partial 반환 (item-detail.blade.php) │ └── routes/api.php에 라우트 추가 (기존 그룹 내) // 기존 Route::middleware(['web', 'auth', 'hq.member']) // ->prefix('admin')->name('api.admin.')->group(function () { ... }); // 내부에 추가: Route::prefix('items')->name('items.')->group(function () { Route::get('/', [ItemManagementApiController::class, 'index'])->name('index'); Route::get('/{id}/bom-tree', [ItemManagementApiController::class, 'bomTree'])->name('bom-tree'); Route::get('/{id}/detail', [ItemManagementApiController::class, 'detail'])->name('detail'); }); ``` ### Step 4: Blade 뷰 생성 (Phase 2.1~2.4) ``` ├── index.blade.php: 3-Panel 메인 레이아웃 │ @extends('layouts.app'), @section('content'), @push('scripts') │ HTMX 페이지이므로 HX-Redirect 필요 (JS가 @push('scripts')에 있음) │ ├── partials/item-list.blade.php: 좌측 품목 리스트 │ @foreach($items as $item) → 품목코드, 품목명, 유형 뱃지 │ data-item-id="{{ $item->id }}" onclick="selectItem({{ $item->id }})" │ ├── partials/bom-tree.blade.php: 중앙 트리 (빈 컨테이너) │
BOM 구성이 없습니다.
'; } }); } // 우측 상세 갱신 (항상) htmx.ajax('GET', `/api/admin/items/${itemId}/detail`, { target: '#item-detail', swap: 'innerHTML', headers: {'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content} }); } // 중앙 트리 노드 클릭 (트리는 유지, 우측만 갱신) function selectTreeNode(itemId) { selectItem(itemId, false); // updateTree = false } ``` --- ## 7. 컨펌 대기 목록 | # | 항목 | 변경 내용 | 영향 범위 | 상태 | |---|------|----------|----------|------| | 1 | 사이드바 메뉴 추가 | "품목관리" 메뉴 항목 추가 | menus 테이블 (DB) | ⏳ tinker 안내 필요 | --- ## 8. 변경 이력 | 날짜 | 항목 | 변경 내용 | 파일 | 승인 | |------|------|----------|------|------| | 2026-02-19 | - | 문서 초안 작성 | - | - | | 2026-02-19 | - | 자기완결성 보강 (Appendix A~C 추가) | - | - | | 2026-02-19 | Phase 1 | Item 모델 보완, ItemDetail/File 모델 생성 | Item.php, ItemDetail.php, File.php | ✅ | | 2026-02-19 | Phase 1 | ItemManagementService 생성 | ItemManagementService.php | ✅ | | 2026-02-19 | Phase 1 | ItemManagementApiController 생성 + API 라우트 | ItemManagementApiController.php, api.php | ✅ | | 2026-02-19 | Phase 2 | 3-Panel Blade 뷰 전체 생성 | index.blade.php + 3 partials | ✅ | | 2026-02-19 | Phase 2 | Web 컨트롤러 + 라우트 등록 | ItemManagementController.php, web.php | ✅ | | 2026-02-19 | - | Phase 1~2 완료, Phase 3 수식 연동 계획 별도 문서 분리 | mng-item-formula-integration-plan.md | - | --- ## 9. 참고 문서 - **품목 정책**: `docs/rules/item-policy.md` - **품목 연동 설계**: `docs/specs/item-master-integration.md` - **MNG 절대 규칙**: `mng/docs/MNG_CRITICAL_RULES.md` - **MNG 프로젝트 문서**: `mng/docs/INDEX.md` - **DB 스키마**: `docs/specs/database-schema.md` - **API Item 모델**: `api/app/Models/Items/Item.php` - **API ItemDetail 모델**: `api/app/Models/Items/ItemDetail.php` --- ## 10. 검증 결과 ### 10.1 테스트 케이스 | 입력값 | 예상 결과 | 실제 결과 | 상태 | |--------|----------|----------|------| | 좌측 검색: "스크린" | "스크린" 포함 품목만 표시 | 정상 동작 | ✅ | | FG 품목 클릭 | 중앙에 BOM 트리, 우측에 상세 | 정상 동작 (정적 BOM 2개 표시) | ✅ | | BOM 없는 품목 클릭 | 중앙 "BOM 없음", 우측 상세 표시 | 정상 동작 | ✅ | | 중앙 트리 노드 클릭 | 우측 상세만 변경 (트리 유지) | 정상 동작 | ✅ | | 테넌트 전환 | 좌측 리스트가 해당 테넌트 품목으로 변경 | 확인 필요 | ⏳ | | 순환 참조 BOM | 무한 루프 없이 maxDepth에서 중단 | 로직 구현 완료, 실제 데이터 미검증 | ⏳ | ### 10.2 성공 기준 | 기준 | 달성 | 비고 | |------|------|------| | 3-Panel 레이아웃 정상 렌더링 | ✅ | 좌측 280px + 중앙 flex-1 + 우측 384px | | 실시간 검색 (debounce 300ms) | ✅ | 코드+이름 동시 검색 | | BOM 재귀 트리 정상 표시 (전체 depth) | ✅ | 펼침/접힘 토글 포함 | | 어디서든 클릭 → 우측 상세 갱신 | ✅ | selectItem + selectTreeNode | | 테넌트 필터링 정상 동작 | ⏳ | withoutGlobalScopes + session 패턴 사용 | | 순환 참조 방지 (maxDepth) | ✅ | visited 배열 + maxDepth 이중 안전장치 | --- ## 11. 자기완결성 점검 결과 ### 11.1 체크리스트 검증 | # | 검증 항목 | 상태 | 비고 | |---|----------|:----:|------| | 1 | 작업 목적이 명확한가? | ✅ | 3-Panel 품목관리 페이지 | | 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 10.2 | | 3 | 작업 범위가 구체적인가? | ✅ | 섹션 4 (12개 작업 항목) | | 4 | 의존성이 명시되어 있는가? | ✅ | items 테이블 존재 전제 | | 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 9 + Appendix | | 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 5 (6 Step) | | 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 10.1 | | 8 | 모호한 표현이 없는가? | ✅ | 구체적 코드/구조 명시 | ### 11.2 새 세션 시뮬레이션 테스트 | 질문 | 답변 가능 | 참조 섹션 | |------|:--------:|----------| | Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | | Q2. 어디서부터 시작해야 하는가? | ✅ | 5. 작업 절차 Step 1 | | Q3. 어떤 파일을 수정해야 하는가? | ✅ | 3.4 파일 구조 + 6.1 기존 파일 현황 | | Q4. 작업 완료 확인 방법은? | ✅ | 10. 검증 결과 | | Q5. 막혔을 때 참고 문서는? | ✅ | 9. 참고 문서 + Appendix A~C | | Q6. MNG 코딩 패턴은 무엇인가? | ✅ | Appendix A (인라인 패턴) | | Q7. 테넌트 필터링은 어떻게 동작하는가? | ✅ | Appendix B (BelongsToTenant 전문) | | Q8. API 모델의 정확한 필드는? | ✅ | Appendix C (API 모델 전문) | **결과**: 8/8 통과 → ✅ 자기완결성 확보 --- ## Appendix A: MNG 코딩 패턴 레퍼런스 > 새 세션에서 외부 파일을 읽지 않고도 MNG 패턴을 따를 수 있도록 인라인화한 레퍼런스. ### A.1 Web Controller 패턴 Web Controller는 Blade 뷰 렌더링만 담당한다. 비즈니스 로직은 Service에 위임. ```php header('HX-Request')) { return response('', 200)->header('HX-Redirect', route('item-management.index')); } return view('item-management.index'); } ``` ### A.2 API Controller 패턴 API Controller는 HTMX 요청 시 HTML partial, 일반 요청 시 JSON 반환. ```php departmentService->getDepartments( $request->all(), $request->integer('per_page', 10) ); // HTMX 요청 시 HTML partial 반환 if ($request->header('HX-Request')) { return view('departments.partials.table', compact('departments')); } // 일반 요청 시 JSON return response()->json([ 'success' => true, 'data' => $departments->items(), 'meta' => [ 'current_page' => $departments->currentPage(), 'last_page' => $departments->lastPage(), 'per_page' => $departments->perPage(), 'total' => $departments->total(), ], ]); } } ``` ### A.3 Service 패턴 모든 DB 쿼리 로직은 Service에서 처리. `session('selected_tenant_id')`로 테넌트 격리. ```php with('parent'); // 검색 필터 if (!empty($filters['search'])) { $search = $filters['search']; $query->where(function ($q) use ($search) { $q->where('name', 'like', "%{$search}%") ->orWhere('code', 'like', "%{$search}%"); }); } return $query->orderBy('sort_order')->paginate($perPage); } } ``` > **중요**: BelongsToTenant trait이 모델에 있으면 tenant_id 필터가 자동 적용된다. > Service에서 수동으로 `where('tenant_id', ...)` 할 필요 없음. ### A.4 Blade + HTMX 패턴 Index 페이지는 빈 셸이고, 데이터는 HTMX `hx-get` + `hx-trigger="load"`로 로드. ```blade {{-- 참고: mng/resources/views/departments/index.blade.php 패턴 --}} @extends('layouts.app') @section('title', '부서 관리') @section('content')