# MNG 견적수식 관리 개발 계획 > **작성일**: 2025-12-22 > **상태**: ✅ 완료 > **대상**: mng.sam.kr/quote-formulas --- ## 1. 현황 분석 ### 1.1 MNG 프로젝트 현재 상태 #### 구현된 기능 (mng) | 기능 | 상태 | 설명 | |-----|------|-----| | 수식 목록 | ✅ 완료 | 페이지네이션, 필터링, HTMX 테이블 | | 수식 생성 | ✅ 완료 | 카테고리, 유형, 변수명, 수식 입력 | | 수식 수정 | ✅ 완료 | 편집 폼, API 연동 | | 수식 삭제 | ✅ 완료 | Soft Delete, 복원, 영구삭제 | | 수식 복제 | ✅ 완료 | 수식 복사 기능 | | 활성/비활성 | ✅ 완료 | 토글 기능 | | 카테고리 관리 | ✅ 완료 | CRUD 구현 | | 시뮬레이터 | ✅ 완료 | 입력값 → 계산 결과 미리보기 | | 변수 참조 | ✅ 완료 | 사용 가능한 변수 목록 표시 | | 수식 검증 | ✅ 완료 | 문법 검증 API | | 범위(Range) 관리 UI | ✅ 완료 | 범위별 결과 설정 화면 (Phase 1) | | 매핑(Mapping) 관리 UI | ✅ 완료 | 매핑 규칙 설정 화면 (Phase 2) | | 품목(Item) 관리 UI | ✅ 완료 | 출력 품목 설정 화면 (Phase 3) | ### 1.2 API 프로젝트 현재 상태 #### 모델 구조 (api) ``` QuoteFormulaCategory (카테고리) └── QuoteFormula (수식) ├── QuoteFormulaRange (범위 조건) ├── QuoteFormulaMapping (매핑 규칙) └── QuoteFormulaItem (출력 품목) ``` #### 시더 데이터 (api) | 시더 | 데이터 수 | 설명 | |-----|---------|-----| | QuoteFormulaCategorySeeder | 11개 | 카테고리 (오픈사이즈~단가수식) | | QuoteFormulaSeeder | 30개 수식, 18개 범위 | 스크린 계산 수식 | | QuoteFormulaItemSeeder | 25개 | 품목 마스터 | #### 서비스 (api) | 서비스 | 역할 | |-------|-----| | QuoteCalculationService | 자동산출 실행 엔진 | | FormulaEvaluatorService | 수식 평가, 범위/매핑 처리 | | QuoteService | 견적 CRUD, 상태 관리 | | QuoteNumberService | 견적번호 생성 | | QuoteDocumentService | PDF/이메일/카카오 발송 (TODO) | --- ## 2. MNG vs API 비교 분석 ### 2.1 데이터 구조 비교 | 항목 | MNG | API | 일치 | |-----|-----|-----|-----| | quote_formula_categories | ✅ | ✅ | ✅ | | quote_formulas | ✅ | ✅ | ✅ | | quote_formula_ranges | ✅ | ✅ | ✅ | | quote_formula_mappings | ✅ | ✅ | ✅ | | quote_formula_items | ✅ | ✅ | ✅ | **결론**: 모델 구조는 동일함 (같은 DB 사용) ### 2.2 기능 비교 | 기능 | MNG | API | 비고 | |-----|-----|-----|-----| | 수식 CRUD | ✅ | ✅ | 동일 | | 카테고리 CRUD | ✅ | ✅ | 동일 | | 범위 관리 UI | ✅ | ✅ (시더) | Phase 1 완료 | | 매핑 관리 UI | ✅ | ✅ (시더) | Phase 2 완료 | | 품목 관리 UI | ✅ | ✅ (시더) | Phase 3 완료 | | 시뮬레이터 | ✅ | ✅ | 동일 | | 자동산출 API | - | ✅ | API 전용 | --- ## 3. 개발 계획 (완료) ### 3.1 목표 MNG에서 **범위(Range), 매핑(Mapping), 품목(Item)** 관리 UI를 추가하여: 1. 시더 없이도 관리자가 직접 수식 규칙 설정 가능 2. SAM 자체 품목 마스터로 가격 설정 3. 실시간 시뮬레이션으로 설정 검증 가능 ### 3.2 개발 범위 (완료) #### Phase 1: 범위(Range) 관리 UI ✅ **우선순위**: 높음 **이유**: 모터, 가이드레일, 케이스 자동 선택에 필수 **기능 목록**: 1. 수식 상세 페이지에 범위 관리 탭 추가 2. 범위 목록 표시 (min ~ max → 결과) 3. 범위 추가/수정/삭제 4. 드래그앤드롭 순서 변경 5. item_code 연결 (품목 선택) **화면 설계**: ``` [수식 수정] 페이지 ├── [기본 정보] 탭 (기존) ├── [범위 설정] 탭 ← 추가 │ ├── 조건 변수: [K (중량)] ▼ │ ├── 범위 목록 │ │ ┌─────────────────────────────────────────────────┐ │ │ │ # │ 최소값 │ 최대값 │ 결과값 │ 품목코드 │ │ │ ├─────────────────────────────────────────────────┤ │ │ │ 1 │ 0 │ 150 │ 150K │ PT-MOTOR-150│ │ │ │ 2 │ 150 │ 300 │ 300K │ PT-MOTOR-300│ │ │ │ 3 │ 300 │ 400 │ 400K │ PT-MOTOR-400│ │ │ └─────────────────────────────────────────────────┘ │ └── [+ 범위 추가] ├── [매핑 설정] 탭 └── [품목 설정] 탭 ``` **API 엔드포인트 (MNG 내부)**: ``` GET /api/admin/quote-formulas/formulas/{id}/ranges POST /api/admin/quote-formulas/formulas/{id}/ranges PUT /api/admin/quote-formulas/formulas/{id}/ranges/{rangeId} DELETE /api/admin/quote-formulas/formulas/{id}/ranges/{rangeId} POST /api/admin/quote-formulas/formulas/{id}/ranges/reorder ``` #### Phase 2: 매핑(Mapping) 관리 UI ✅ **우선순위**: 중간 **이유**: 제어기 유형 등 코드 매핑에 사용 **기능 목록**: 1. 수식 상세 페이지에 매핑 관리 탭 추가 2. 매핑 목록 표시 (소스값 → 결과값) 3. 매핑 추가/수정/삭제 **화면 설계**: ``` [매핑 설정] 탭 ├── 소스 변수: [CONTROL_TYPE] ▼ ├── 매핑 목록 │ ┌──────────────────────────────────────────────────┐ │ │ # │ 소스값 │ 결과값 │ 품목코드 │ │ ├──────────────────────────────────────────────────┤ │ │ 1 │ EMB │ 매립형 │ PT-CTRL-EMB │ │ │ 2 │ EXP │ 노출형 │ PT-CTRL-EXP │ │ │ 3 │ BOX_1P │ 콘트롤박스 │ PT-CTRL-BOX-1P │ │ └──────────────────────────────────────────────────┘ └── [+ 매핑 추가] ``` #### Phase 3: 품목(Item) 관리 UI ✅ **우선순위**: 중간 **이유**: 수식 결과로 생성되는 품목 정의 **기능 목록**: 1. 수식 상세 페이지에 품목 관리 탭 추가 2. 품목 목록 표시 3. 품목 추가/수정/삭제 4. 수량/단가 수식 입력 5. SAM 품목 마스터에서 가격 참조 **화면 설계**: ``` [품목 설정] 탭 ├── 품목 목록 │ ┌───────────────────────────────────────────────────────────┐ │ │ 품목코드 │ 품목명 │ 규격 │ 수량식 │ 단가식│ │ ├───────────────────────────────────────────────────────────┤ │ │ PT-MOTOR-150 │ 개폐전동기 150kg│ 150K(S) │ 1 │ 285000│ │ │ PT-GR-3000 │ 가이드레일 3000 │ 3000mm │ 2 │ 42000 │ │ └───────────────────────────────────────────────────────────┘ └── [+ 품목 추가] ``` ### 3.3 파일 구조 (구현 완료) #### Controllers ``` app/Http/Controllers/ ├── QuoteFormulaController.php (수정: 탭 추가) └── Api/Admin/Quote/ ├── QuoteFormulaController.php ├── QuoteFormulaRangeController.php ✅ ├── QuoteFormulaMappingController.php ✅ ├── QuoteFormulaItemController.php ✅ └── QuoteFormulaCategoryController.php ``` #### Services ``` app/Services/Quote/ ├── QuoteFormulaService.php ├── QuoteFormulaRangeService.php ✅ ├── QuoteFormulaMappingService.php ✅ ├── QuoteFormulaItemService.php ✅ └── QuoteFormulaCategoryService.php ``` #### Views ``` resources/views/quote-formulas/ ├── index.blade.php ├── create.blade.php ├── edit.blade.php (수정: 탭 구조) ├── simulator.blade.php └── partials/ ├── basic-info-tab.blade.php ✅ ├── ranges-tab.blade.php ✅ ├── mappings-tab.blade.php ✅ └── items-tab.blade.php ✅ ``` --- ## 4. 기술 스택 ### 4.1 Frontend (MNG) - **Framework**: Laravel Blade + Alpine.js - **Styling**: Tailwind CSS + DaisyUI - **AJAX**: HTMX (hx-get, hx-post, hx-delete) - **Modal**: DaisyUI modal 컴포넌트 ### 4.2 Backend (MNG) - **Framework**: Laravel 12 - **ORM**: Eloquent - **DB**: MySQL (samdb) - **Auth**: Session 기반 ### 4.3 API 연동 - MNG 내부 API (`/api/admin/quote-formulas/*`) --- ## 5. 검증 계획 ### 5.1 시뮬레이터 테스트 ``` 입력: W0=3000, H0=2500 예상 결과: - CASE: PT-CASE-3600 (S=3270) - GR: PT-GR-3000 (H1=2770) - MOTOR: PT-MOTOR-150 (K=41.21kg) ``` ### 5.2 CRUD 테스트 - 범위 추가/수정/삭제 후 시뮬레이터 결과 확인 - 품목 가격 변경 후 합계 확인 --- ## 6. 참고 자료 ### 6.1 파일 위치 (MNG) ``` mng/ ├── app/Http/Controllers/ │ ├── QuoteFormulaController.php │ └── Api/Admin/Quote/ │ ├── QuoteFormulaController.php │ ├── QuoteFormulaRangeController.php │ ├── QuoteFormulaMappingController.php │ ├── QuoteFormulaItemController.php │ └── QuoteFormulaCategoryController.php ├── app/Services/Quote/ │ ├── QuoteFormulaService.php │ ├── QuoteFormulaRangeService.php │ ├── QuoteFormulaMappingService.php │ ├── QuoteFormulaItemService.php │ └── QuoteFormulaCategoryService.php ├── app/Models/Quote/ │ ├── QuoteFormula.php │ ├── QuoteFormulaCategory.php │ ├── QuoteFormulaRange.php │ ├── QuoteFormulaMapping.php │ └── QuoteFormulaItem.php └── resources/views/quote-formulas/ ├── index.blade.php ├── create.blade.php ├── edit.blade.php ├── simulator.blade.php └── partials/ ├── basic-info-tab.blade.php ├── ranges-tab.blade.php ├── mappings-tab.blade.php └── items-tab.blade.php ``` ### 6.2 API 시더 위치 ``` api/database/seeders/ ├── QuoteFormulaCategorySeeder.php ├── QuoteFormulaSeeder.php └── QuoteFormulaItemSeeder.php ``` --- ## 7. 코딩 컨벤션 및 예시 코드 ### 7.1 API Controller 패턴 (MNG) ```php rangeService->getRangesByFormula($formulaId); return response()->json([ 'success' => true, 'data' => $ranges, ]); } /** * 범위 생성 */ public function store(Request $request, int $formulaId): JsonResponse { $validated = $request->validate([ 'min_value' => 'nullable|numeric', 'max_value' => 'nullable|numeric', 'condition_variable' => 'required|string|max:50', 'result_value' => 'required|string', 'result_type' => 'in:fixed,formula', 'sort_order' => 'nullable|integer', ]); $range = $this->rangeService->createRange($formulaId, $validated); return response()->json([ 'success' => true, 'message' => '범위가 추가되었습니다.', 'data' => $range, ]); } /** * 범위 수정 */ public function update(Request $request, int $formulaId, int $rangeId): JsonResponse { $validated = $request->validate([ 'min_value' => 'nullable|numeric', 'max_value' => 'nullable|numeric', 'result_value' => 'required|string', 'result_type' => 'in:fixed,formula', ]); $this->rangeService->updateRange($rangeId, $validated); return response()->json([ 'success' => true, 'message' => '범위가 수정되었습니다.', ]); } /** * 범위 삭제 */ public function destroy(int $formulaId, int $rangeId): JsonResponse { $this->rangeService->deleteRange($rangeId); return response()->json([ 'success' => true, 'message' => '범위가 삭제되었습니다.', ]); } /** * 순서 변경 */ public function reorder(Request $request, int $formulaId): JsonResponse { $validated = $request->validate([ 'range_ids' => 'required|array', 'range_ids.*' => 'integer', ]); $this->rangeService->reorder($validated['range_ids']); return response()->json([ 'success' => true, 'message' => '순서가 변경되었습니다.', ]); } } ``` ### 7.2 Service 패턴 (MNG) ```php orderBy('sort_order') ->get(); } /** * 범위 생성 */ public function createRange(int $formulaId, array $data): QuoteFormulaRange { $data['formula_id'] = $formulaId; // 순서 자동 설정 if (!isset($data['sort_order'])) { $maxOrder = QuoteFormulaRange::where('formula_id', $formulaId)->max('sort_order') ?? 0; $data['sort_order'] = $maxOrder + 1; } return QuoteFormulaRange::create($data); } /** * 범위 수정 */ public function updateRange(int $rangeId, array $data): QuoteFormulaRange { $range = QuoteFormulaRange::findOrFail($rangeId); $range->update($data); return $range->fresh(); } /** * 범위 삭제 */ public function deleteRange(int $rangeId): void { QuoteFormulaRange::destroy($rangeId); } /** * 순서 변경 */ public function reorder(array $rangeIds): void { foreach ($rangeIds as $order => $id) { QuoteFormulaRange::where('id', $id)->update(['sort_order' => $order + 1]); } } } ``` ### 7.3 API 응답 형식 ```json // 성공 응답 { "success": true, "message": "범위가 추가되었습니다.", "data": { ... } } // 실패 응답 { "success": false, "message": "이미 사용 중인 변수명입니다." } // 목록 응답 { "success": true, "data": [ { "id": 1, "formula_id": 5, "min_value": "0.0000", "max_value": "150.0000", "condition_variable": "K", "result_value": "{\"value\":\"150K\",\"item_code\":\"PT-MOTOR-150\"}", "result_type": "fixed", "sort_order": 1 } ] } ``` --- ## 8. 체크리스트 (완료) ### 개발 완료 확인 - [x] mng 프로젝트 디렉토리: `/Users/hskwon/Works/@KD_SAM/SAM/mng` - [x] `QuoteFormulaRangeController.php` 생성 - [x] `QuoteFormulaRangeService.php` 생성 - [x] `QuoteFormulaMappingController.php` 생성 - [x] `QuoteFormulaMappingService.php` 생성 - [x] `QuoteFormulaItemController.php` 생성 - [x] `QuoteFormulaItemService.php` 생성 - [x] `routes/api.php`에 라우트 추가 - [x] `edit.blade.php` 탭 구조로 수정 - [x] `partials/ranges-tab.blade.php` 생성 - [x] `partials/mappings-tab.blade.php` 생성 - [x] `partials/items-tab.blade.php` 생성 --- *문서 버전*: 2.0 *작성자*: Claude Code *검토자*: - *최종 업데이트*: 2025-12-22 (Phase 1-3 완료, 5130 연동 제거)