docs: MNG 견적수식 관리 개발 계획 문서 추가
- Phase 1-4 개발 계획 (범위/매핑/품목 관리 UI, 5130 연동) - MNG vs API 시스템 비교 분석 - 코딩 컨벤션 및 예시 코드 (Controller, Service, Blade View) - DB 스키마 DDL 및 API 응답 포맷 - 새 세션용 체크리스트 포함 (v1.1)
This commit is contained in:
952
plans/mng-quote-formula-development-plan.md
Normal file
952
plans/mng-quote-formula-development-plan.md
Normal file
@@ -0,0 +1,952 @@
|
||||
# MNG 견적수식 관리 개발 계획
|
||||
|
||||
> **작성일**: 2025-12-22
|
||||
> **상태**: 계획 수립
|
||||
> **대상**: mng.sam.kr/quote-formulas
|
||||
|
||||
---
|
||||
|
||||
## 1. 현황 분석
|
||||
|
||||
### 1.1 MNG 프로젝트 현재 상태
|
||||
|
||||
#### 구현된 기능 (mng)
|
||||
|
||||
| 기능 | 상태 | 설명 |
|
||||
|-----|------|-----|
|
||||
| 수식 목록 | ✅ 완료 | 페이지네이션, 필터링, HTMX 테이블 |
|
||||
| 수식 생성 | ✅ 완료 | 카테고리, 유형, 변수명, 수식 입력 |
|
||||
| 수식 수정 | ✅ 완료 | 편집 폼, API 연동 |
|
||||
| 수식 삭제 | ✅ 완료 | Soft Delete, 복원, 영구삭제 |
|
||||
| 수식 복제 | ✅ 완료 | 수식 복사 기능 |
|
||||
| 활성/비활성 | ✅ 완료 | 토글 기능 |
|
||||
| 카테고리 관리 | ✅ 완료 | CRUD 구현 |
|
||||
| 시뮬레이터 | ✅ 완료 | 입력값 → 계산 결과 미리보기 |
|
||||
| 변수 참조 | ✅ 완료 | 사용 가능한 변수 목록 표시 |
|
||||
| 수식 검증 | ✅ 완료 | 문법 검증 API |
|
||||
|
||||
#### 미구현/미완성 기능 (mng)
|
||||
|
||||
| 기능 | 상태 | 설명 |
|
||||
|-----|------|-----|
|
||||
| 범위(Range) 관리 UI | ❌ 미구현 | 범위별 결과 설정 화면 없음 |
|
||||
| 매핑(Mapping) 관리 UI | ❌ 미구현 | 매핑 규칙 설정 화면 없음 |
|
||||
| 품목(Item) 관리 UI | ❌ 미구현 | 출력 품목 설정 화면 없음 |
|
||||
| 5130 데이터 연동 | ❌ 미구현 | 레거시 가격 데이터 동기화 |
|
||||
|
||||
### 1.2 API 프로젝트 현재 상태
|
||||
|
||||
#### 모델 구조 (api)
|
||||
|
||||
```
|
||||
QuoteFormulaCategory (카테고리)
|
||||
└── QuoteFormula (수식)
|
||||
├── QuoteFormulaRange (범위 조건)
|
||||
├── QuoteFormulaMapping (매핑 규칙)
|
||||
└── QuoteFormulaItem (출력 품목)
|
||||
```
|
||||
|
||||
#### 시더 데이터 (api)
|
||||
|
||||
| 시더 | 데이터 수 | 설명 |
|
||||
|-----|---------|-----|
|
||||
| QuoteFormulaCategorySeeder | 11개 | 카테고리 (오픈사이즈~단가수식) |
|
||||
| QuoteFormulaSeeder | 30개 수식, 18개 범위 | 스크린 계산 수식 |
|
||||
| QuoteFormulaItemSeeder | 25개 | 품목 마스터 (5130 가격 적용) |
|
||||
|
||||
#### 서비스 (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** | ❌ | ✅ (시더) | MNG에 UI 필요 |
|
||||
| **매핑 관리 UI** | ❌ | ✅ (시더) | MNG에 UI 필요 |
|
||||
| **품목 관리 UI** | ❌ | ✅ (시더) | MNG에 UI 필요 |
|
||||
| 시뮬레이터 | ✅ | ✅ | 동일 |
|
||||
| 자동산출 API | - | ✅ | API 전용 |
|
||||
|
||||
### 2.3 핵심 차이점
|
||||
|
||||
```
|
||||
MNG (관리 UI)
|
||||
├── 수식 기본 정보 관리 ✅
|
||||
├── 카테고리 관리 ✅
|
||||
├── 시뮬레이터 ✅
|
||||
└── 범위/매핑/품목 관리 ❌ ← 개발 필요
|
||||
|
||||
API (자동산출 엔진)
|
||||
├── 시더로 데이터 주입 ✅
|
||||
├── 자동산출 서비스 ✅
|
||||
└── 견적 생성 API ✅
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 개발 계획
|
||||
|
||||
### 3.1 목표
|
||||
|
||||
MNG에서 **범위(Range), 매핑(Mapping), 품목(Item)** 관리 UI를 추가하여:
|
||||
1. 시더 없이도 관리자가 직접 수식 규칙 설정 가능
|
||||
2. 5130 레거시 데이터를 참조하여 가격 설정 가능
|
||||
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. 5130 품목 검색 및 가격 참조
|
||||
|
||||
**화면 설계**:
|
||||
```
|
||||
[품목 설정] 탭
|
||||
├── 품목 목록
|
||||
│ ┌───────────────────────────────────────────────────────────┐
|
||||
│ │ 품목코드 │ 품목명 │ 규격 │ 수량식 │ 단가식│
|
||||
│ ├───────────────────────────────────────────────────────────┤
|
||||
│ │ PT-MOTOR-150 │ 개폐전동기 150kg│ 150K(S) │ 1 │ 285000│
|
||||
│ │ PT-GR-3000 │ 가이드레일 3000 │ 3000mm │ 2 │ 42000 │
|
||||
│ └───────────────────────────────────────────────────────────┘
|
||||
├── [+ 품목 추가]
|
||||
└── [5130에서 가져오기] ← 레거시 연동
|
||||
```
|
||||
|
||||
#### Phase 4: 5130 데이터 연동
|
||||
|
||||
**우선순위**: 높음
|
||||
**이유**: 실제 가격 데이터 필요
|
||||
|
||||
**기능 목록**:
|
||||
1. 5130 DB 가격 테이블 조회 API
|
||||
2. 품목 추가 시 5130 검색 모달
|
||||
3. 가격 동기화 기능
|
||||
4. 가격 변경 이력 관리
|
||||
|
||||
**5130 테이블 참조**:
|
||||
```sql
|
||||
-- chandj.price_motor: 모터 가격
|
||||
-- chandj.price_*: 기타 품목 가격
|
||||
```
|
||||
|
||||
### 3.3 파일 수정/추가 목록
|
||||
|
||||
#### Routes (mng/routes/web.php)
|
||||
```php
|
||||
// 기존 라우트에 추가
|
||||
Route::prefix('quote-formulas')->group(function () {
|
||||
// ... 기존 라우트
|
||||
Route::get('/{id}/ranges', [QuoteFormulaController::class, 'ranges']);
|
||||
Route::get('/{id}/mappings', [QuoteFormulaController::class, 'mappings']);
|
||||
Route::get('/{id}/items', [QuoteFormulaController::class, 'items']);
|
||||
});
|
||||
```
|
||||
|
||||
#### API Routes (mng/routes/api.php)
|
||||
```php
|
||||
// 범위/매핑/품목 CRUD API
|
||||
Route::prefix('quote-formulas/formulas/{formulaId}')->group(function () {
|
||||
Route::apiResource('ranges', QuoteFormulaRangeController::class);
|
||||
Route::post('ranges/reorder', [QuoteFormulaRangeController::class, 'reorder']);
|
||||
|
||||
Route::apiResource('mappings', QuoteFormulaMappingController::class);
|
||||
|
||||
Route::apiResource('items', QuoteFormulaItemController::class);
|
||||
});
|
||||
|
||||
// 5130 연동
|
||||
Route::prefix('legacy')->group(function () {
|
||||
Route::get('items/search', [LegacyController::class, 'searchItems']);
|
||||
Route::get('prices/{itemCode}', [LegacyController::class, 'getPrice']);
|
||||
});
|
||||
```
|
||||
|
||||
#### Controllers
|
||||
```
|
||||
app/Http/Controllers/
|
||||
├── QuoteFormulaController.php (수정: 탭 추가)
|
||||
└── Api/Admin/Quote/
|
||||
├── QuoteFormulaRangeController.php (신규)
|
||||
├── QuoteFormulaMappingController.php (신규)
|
||||
├── QuoteFormulaItemController.php (신규)
|
||||
└── LegacyController.php (신규: 5130 연동)
|
||||
```
|
||||
|
||||
#### Services
|
||||
```
|
||||
app/Services/
|
||||
├── Quote/
|
||||
│ ├── QuoteFormulaRangeService.php (신규)
|
||||
│ ├── QuoteFormulaMappingService.php (신규)
|
||||
│ └── QuoteFormulaItemService.php (신규)
|
||||
└── Legacy/
|
||||
└── LegacyPriceService.php (신규: 5130 가격 조회)
|
||||
```
|
||||
|
||||
#### Views
|
||||
```
|
||||
resources/views/quote-formulas/
|
||||
├── edit.blade.php (수정: 탭 구조로 변경)
|
||||
├── partials/
|
||||
│ ├── ranges-tab.blade.php (신규)
|
||||
│ ├── mappings-tab.blade.php (신규)
|
||||
│ └── items-tab.blade.php (신규)
|
||||
└── modals/
|
||||
├── range-form.blade.php (신규)
|
||||
├── mapping-form.blade.php (신규)
|
||||
├── item-form.blade.php (신규)
|
||||
└── legacy-item-search.blade.php (신규)
|
||||
```
|
||||
|
||||
### 3.4 개발 순서
|
||||
|
||||
```
|
||||
Phase 1: 범위 관리 (1주)
|
||||
├── Day 1-2: API 엔드포인트 구현
|
||||
├── Day 3-4: UI 컴포넌트 구현
|
||||
└── Day 5: 테스트 및 검증
|
||||
|
||||
Phase 2: 매핑 관리 (0.5주)
|
||||
├── Day 1: API 구현
|
||||
└── Day 2-3: UI 구현
|
||||
|
||||
Phase 3: 품목 관리 (0.5주)
|
||||
├── Day 1: API 구현
|
||||
└── Day 2-3: UI 구현
|
||||
|
||||
Phase 4: 5130 연동 (1주)
|
||||
├── Day 1-2: 레거시 DB 조회 서비스
|
||||
├── Day 3-4: 검색 모달 UI
|
||||
└── Day 5: 가격 동기화 기능
|
||||
|
||||
통합 테스트 (0.5주)
|
||||
├── 시뮬레이터 연동 테스트
|
||||
└── 전체 플로우 검증
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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 + chandj)
|
||||
- **Auth**: Session 기반
|
||||
|
||||
### 4.3 API 연동
|
||||
- MNG 내부 API (`/api/admin/quote-formulas/*`)
|
||||
- 5130 DB 직접 조회 (chandj 데이터베이스)
|
||||
|
||||
---
|
||||
|
||||
## 5. 데이터 마이그레이션
|
||||
|
||||
### 5.1 현재 상태
|
||||
- API 시더로 30개 수식, 18개 범위, 25개 품목 등록됨
|
||||
- 5130 실제 가격 데이터 반영 완료
|
||||
|
||||
### 5.2 마이그레이션 계획
|
||||
1. **Phase 1 완료 후**: 시더 데이터를 MNG UI로 확인 가능
|
||||
2. **Phase 4 완료 후**: 5130 데이터 자동 동기화
|
||||
|
||||
---
|
||||
|
||||
## 6. 검증 계획
|
||||
|
||||
### 6.1 시뮬레이터 테스트
|
||||
```
|
||||
입력: W0=3000, H0=2500
|
||||
예상 결과:
|
||||
- CASE: PT-CASE-3600 (S=3270)
|
||||
- GR: PT-GR-3000 (H1=2770)
|
||||
- MOTOR: PT-MOTOR-150 (K=41.21kg)
|
||||
```
|
||||
|
||||
### 6.2 CRUD 테스트
|
||||
- 범위 추가/수정/삭제 후 시뮬레이터 결과 확인
|
||||
- 품목 가격 변경 후 합계 확인
|
||||
|
||||
---
|
||||
|
||||
## 7. 참고 자료
|
||||
|
||||
### 7.1 기존 파일 위치 (MNG)
|
||||
```
|
||||
mng/
|
||||
├── app/Http/Controllers/
|
||||
│ ├── QuoteFormulaController.php
|
||||
│ └── Api/Admin/Quote/QuoteFormulaController.php
|
||||
├── app/Services/Quote/
|
||||
│ └── QuoteFormulaService.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
|
||||
```
|
||||
|
||||
### 7.2 API 시더 위치
|
||||
```
|
||||
api/database/seeders/
|
||||
├── QuoteFormulaCategorySeeder.php
|
||||
├── QuoteFormulaSeeder.php
|
||||
└── QuoteFormulaItemSeeder.php
|
||||
```
|
||||
|
||||
### 7.3 5130 가격 테이블
|
||||
```sql
|
||||
chandj.price_motor -- 모터 가격
|
||||
chandj.price_* -- 기타 품목 가격 (확인 필요)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 리스크 및 대응
|
||||
|
||||
| 리스크 | 영향 | 대응 |
|
||||
|-------|-----|-----|
|
||||
| 5130 DB 스키마 변경 | 중 | JSON 파싱 로직 유연하게 구현 |
|
||||
| MNG-API 데이터 불일치 | 높 | 동일 DB 사용으로 해결됨 |
|
||||
| 시뮬레이터 성능 저하 | 낮 | 수식 캐싱 적용 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 다음 단계
|
||||
|
||||
1. **승인 요청**: 이 계획에 대한 검토 및 승인
|
||||
2. **Phase 1 착수**: 범위 관리 UI 개발 시작
|
||||
3. **주간 리뷰**: 진행 상황 점검
|
||||
|
||||
---
|
||||
|
||||
## 10. 코딩 컨벤션 및 예시 코드
|
||||
|
||||
### 10.1 API Controller 패턴 (MNG)
|
||||
|
||||
```php
|
||||
<?php
|
||||
// 파일: app/Http/Controllers/Api/Admin/Quote/QuoteFormulaRangeController.php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin\Quote;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\Quote\QuoteFormulaRangeService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class QuoteFormulaRangeController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly QuoteFormulaRangeService $rangeService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 범위 목록 조회
|
||||
*/
|
||||
public function index(int $formulaId): JsonResponse
|
||||
{
|
||||
$ranges = $this->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' => '순서가 변경되었습니다.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 10.2 Service 패턴 (MNG)
|
||||
|
||||
```php
|
||||
<?php
|
||||
// 파일: app/Services/Quote/QuoteFormulaRangeService.php
|
||||
|
||||
namespace App\Services\Quote;
|
||||
|
||||
use App\Models\Quote\QuoteFormulaRange;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class QuoteFormulaRangeService
|
||||
{
|
||||
/**
|
||||
* 수식별 범위 조회
|
||||
*/
|
||||
public function getRangesByFormula(int $formulaId): Collection
|
||||
{
|
||||
return QuoteFormulaRange::where('formula_id', $formulaId)
|
||||
->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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 10.3 Model 구조 (참조용)
|
||||
|
||||
```php
|
||||
<?php
|
||||
// 파일: app/Models/Quote/QuoteFormulaRange.php (기존)
|
||||
|
||||
class QuoteFormulaRange extends Model
|
||||
{
|
||||
protected $table = 'quote_formula_ranges';
|
||||
|
||||
protected $fillable = [
|
||||
'formula_id',
|
||||
'min_value',
|
||||
'max_value',
|
||||
'condition_variable',
|
||||
'result_value', // JSON 저장 가능: {"value": "150K", "item_code": "PT-MOTOR-150"}
|
||||
'result_type', // 'fixed' | 'formula'
|
||||
'sort_order',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'min_value' => 'decimal:4',
|
||||
'max_value' => 'decimal:4',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
public function formula(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(QuoteFormula::class, 'formula_id');
|
||||
}
|
||||
|
||||
public function isInRange($value): bool
|
||||
{
|
||||
// min < value <= max 체크
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 10.4 Blade View 패턴 (HTMX + Alpine.js)
|
||||
|
||||
```blade
|
||||
{{-- 파일: resources/views/quote-formulas/partials/ranges-tab.blade.php --}}
|
||||
|
||||
<div x-data="rangesManager()" x-init="loadRanges()">
|
||||
{{-- 헤더 --}}
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-medium">범위 설정</h3>
|
||||
<button @click="openAddModal()" class="btn btn-primary btn-sm">
|
||||
+ 범위 추가
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- 조건 변수 표시 --}}
|
||||
<div class="mb-4">
|
||||
<span class="text-sm text-gray-600">조건 변수:</span>
|
||||
<span class="font-mono bg-gray-100 px-2 py-1 rounded" x-text="conditionVariable || '-'"></span>
|
||||
</div>
|
||||
|
||||
{{-- 범위 목록 테이블 --}}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-12">#</th>
|
||||
<th>최소값</th>
|
||||
<th>최대값</th>
|
||||
<th>결과값</th>
|
||||
<th>품목코드</th>
|
||||
<th class="w-24">액션</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="(range, index) in ranges" :key="range.id">
|
||||
<tr>
|
||||
<td x-text="index + 1"></td>
|
||||
<td x-text="range.min_value ?? '-'"></td>
|
||||
<td x-text="range.max_value ?? '-'"></td>
|
||||
<td>
|
||||
<span class="font-mono" x-text="getResultDisplay(range)"></span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-ghost" x-text="getItemCode(range) || '-'"></span>
|
||||
</td>
|
||||
<td>
|
||||
<button @click="editRange(range)" class="btn btn-ghost btn-xs">수정</button>
|
||||
<button @click="deleteRange(range.id)" class="btn btn-ghost btn-xs text-red-500">삭제</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- 빈 상태 --}}
|
||||
<div x-show="ranges.length === 0" class="text-center py-8 text-gray-500">
|
||||
설정된 범위가 없습니다.
|
||||
</div>
|
||||
|
||||
{{-- 범위 추가/수정 모달 --}}
|
||||
<dialog id="rangeModal" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg" x-text="editingRange ? '범위 수정' : '범위 추가'"></h3>
|
||||
<form @submit.prevent="saveRange()">
|
||||
<div class="grid grid-cols-2 gap-4 mt-4">
|
||||
<div>
|
||||
<label class="label">최소값</label>
|
||||
<input type="number" step="0.0001" x-model="form.min_value"
|
||||
class="input input-bordered w-full">
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">최대값</label>
|
||||
<input type="number" step="0.0001" x-model="form.max_value"
|
||||
class="input input-bordered w-full">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<label class="label">결과값 (JSON)</label>
|
||||
<textarea x-model="form.result_value" rows="3"
|
||||
class="textarea textarea-bordered w-full font-mono"
|
||||
placeholder='{"value": "150K", "item_code": "PT-MOTOR-150"}'></textarea>
|
||||
</div>
|
||||
<div class="modal-action">
|
||||
<button type="button" @click="closeModal()" class="btn">취소</button>
|
||||
<button type="submit" class="btn btn-primary">저장</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function rangesManager() {
|
||||
return {
|
||||
ranges: [],
|
||||
conditionVariable: '',
|
||||
editingRange: null,
|
||||
form: {
|
||||
min_value: null,
|
||||
max_value: null,
|
||||
result_value: '',
|
||||
result_type: 'fixed',
|
||||
},
|
||||
|
||||
async loadRanges() {
|
||||
const formulaId = {{ $formula->id }};
|
||||
const res = await fetch(`/api/admin/quote-formulas/formulas/${formulaId}/ranges`);
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
this.ranges = data.data;
|
||||
if (this.ranges.length > 0) {
|
||||
this.conditionVariable = this.ranges[0].condition_variable;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getResultDisplay(range) {
|
||||
try {
|
||||
const parsed = JSON.parse(range.result_value);
|
||||
return parsed.value || range.result_value;
|
||||
} catch {
|
||||
return range.result_value;
|
||||
}
|
||||
},
|
||||
|
||||
getItemCode(range) {
|
||||
try {
|
||||
const parsed = JSON.parse(range.result_value);
|
||||
return parsed.item_code;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
openAddModal() {
|
||||
this.editingRange = null;
|
||||
this.form = { min_value: null, max_value: null, result_value: '', result_type: 'fixed' };
|
||||
document.getElementById('rangeModal').showModal();
|
||||
},
|
||||
|
||||
editRange(range) {
|
||||
this.editingRange = range;
|
||||
this.form = { ...range };
|
||||
document.getElementById('rangeModal').showModal();
|
||||
},
|
||||
|
||||
closeModal() {
|
||||
document.getElementById('rangeModal').close();
|
||||
},
|
||||
|
||||
async saveRange() {
|
||||
const formulaId = {{ $formula->id }};
|
||||
const url = this.editingRange
|
||||
? `/api/admin/quote-formulas/formulas/${formulaId}/ranges/${this.editingRange.id}`
|
||||
: `/api/admin/quote-formulas/formulas/${formulaId}/ranges`;
|
||||
const method = this.editingRange ? 'PUT' : 'POST';
|
||||
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
},
|
||||
body: JSON.stringify(this.form),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
this.closeModal();
|
||||
await this.loadRanges();
|
||||
showToast(data.message, 'success');
|
||||
} else {
|
||||
showToast(data.message, 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async deleteRange(rangeId) {
|
||||
if (!confirm('이 범위를 삭제하시겠습니까?')) return;
|
||||
|
||||
const formulaId = {{ $formula->id }};
|
||||
const res = await fetch(`/api/admin/quote-formulas/formulas/${formulaId}/ranges/${rangeId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
await this.loadRanges();
|
||||
showToast(data.message, 'success');
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 10.5 DB 스키마 (참조용)
|
||||
|
||||
```sql
|
||||
-- quote_formula_ranges 테이블 (기존)
|
||||
CREATE TABLE `quote_formula_ranges` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`formula_id` BIGINT UNSIGNED NOT NULL,
|
||||
`min_value` DECIMAL(15,4) NULL COMMENT '최소값 (NULL=제한없음)',
|
||||
`max_value` DECIMAL(15,4) NULL COMMENT '최대값 (NULL=제한없음)',
|
||||
`condition_variable` VARCHAR(50) NOT NULL COMMENT '조건 변수 (K, H1, S 등)',
|
||||
`result_value` TEXT NOT NULL COMMENT '결과값 (JSON 또는 문자열)',
|
||||
`result_type` ENUM('fixed','formula') DEFAULT 'fixed' COMMENT '결과 유형',
|
||||
`sort_order` INT DEFAULT 0,
|
||||
`created_at` TIMESTAMP NULL,
|
||||
`updated_at` TIMESTAMP NULL,
|
||||
|
||||
INDEX `idx_formula_id` (`formula_id`),
|
||||
CONSTRAINT `fk_ranges_formula` FOREIGN KEY (`formula_id`)
|
||||
REFERENCES `quote_formulas`(`id`) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- quote_formula_items 테이블 (기존)
|
||||
CREATE TABLE `quote_formula_items` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`formula_id` BIGINT UNSIGNED NOT NULL,
|
||||
`item_code` VARCHAR(50) NOT NULL COMMENT '품목 코드',
|
||||
`item_name` VARCHAR(200) NOT NULL COMMENT '품목명',
|
||||
`specification` VARCHAR(200) NULL COMMENT '규격',
|
||||
`unit` VARCHAR(20) NOT NULL COMMENT '단위',
|
||||
`quantity_formula` VARCHAR(500) NOT NULL COMMENT '수량 계산식',
|
||||
`unit_price_formula` VARCHAR(500) NULL COMMENT '단가 계산식 (NULL=마스터 참조)',
|
||||
`sort_order` INT DEFAULT 0,
|
||||
`created_at` TIMESTAMP NULL,
|
||||
`updated_at` TIMESTAMP NULL,
|
||||
|
||||
INDEX `idx_formula_id` (`formula_id`),
|
||||
INDEX `idx_item_code` (`item_code`),
|
||||
CONSTRAINT `fk_items_formula` FOREIGN KEY (`formula_id`)
|
||||
REFERENCES `quote_formulas`(`id`) ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
### 10.6 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
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 체크리스트 (새 세션용)
|
||||
|
||||
### 개발 시작 전 확인
|
||||
|
||||
- [ ] mng 프로젝트 디렉토리 확인: `/Users/hskwon/Works/@KD_SAM/SAM/mng`
|
||||
- [ ] 기존 Controller 패턴 확인: `app/Http/Controllers/Api/Admin/Quote/QuoteFormulaController.php`
|
||||
- [ ] 기존 Model 확인: `app/Models/Quote/QuoteFormulaRange.php`
|
||||
- [ ] 기존 View 확인: `resources/views/quote-formulas/edit.blade.php`
|
||||
- [ ] DB 테이블 확인: `quote_formula_ranges`, `quote_formula_items`
|
||||
|
||||
### Phase 1 (범위 관리) 작업 순서
|
||||
|
||||
1. [ ] `QuoteFormulaRangeController.php` 생성
|
||||
2. [ ] `QuoteFormulaRangeService.php` 생성
|
||||
3. [ ] `routes/api.php`에 라우트 추가
|
||||
4. [ ] `edit.blade.php` 탭 구조로 수정
|
||||
5. [ ] `partials/ranges-tab.blade.php` 생성
|
||||
6. [ ] HTMX/Alpine.js 연동 테스트
|
||||
7. [ ] 시뮬레이터와 연동 확인
|
||||
|
||||
---
|
||||
|
||||
*문서 버전*: 1.1
|
||||
*작성자*: Claude Code
|
||||
*검토자*: -
|
||||
*업데이트*: 코딩 컨벤션 및 예시 코드 추가 (2025-12-22)
|
||||
Reference in New Issue
Block a user