Files
sam-docs/features/sales/products.md
김보곤 b6edc6db27 docs: [영업] 최저가 정책 및 개발비-구독료 연동 문서 반영
- customer-pricing.md: 최저가 정책, 개발비-구독료 연동 비율 섹션 추가
- partner-commission.md: 최저가와 수당 관계, 시뮬레이터 안내 섹션 추가
- products.md: min_development_fee/min_subscription_fee 필드, 시뮬레이터 연동 섹션 추가
2026-03-14 15:25:51 +09:00

253 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 상품관리
## 개요
상품관리는 영업에 사용되는 상품(프로그램)을 카테고리별로 등록하고 가격/수당률을 설정하는 기능입니다.
카테고리 관리, 상품 CRUD, 순서 조정, 가격/수당률 설정, 영업 시나리오 연동을 지원합니다.
- **라우트**: `GET /sales/products`
- **미들웨어**: `auth`, `hq.member` (본사 전용)
- **UI 기술**: Blade + Alpine.js + HTMX + Tailwind CSS
## 파일 구조
```
mng/
├── app/Http/Controllers/Sales/
│ └── SalesProductController.php # 메인 컨트롤러 (12개 메서드)
├── app/Models/Sales/
│ ├── SalesProduct.php # 상품 모델
│ └── SalesProductCategory.php # 카테고리 모델
└── resources/views/sales/products/
├── index.blade.php # 메인 페이지
└── partials/
└── product-list.blade.php # 상품 목록 파셜 (HTMX)
api/
└── database/migrations/
├── 2026_01_29_150000_create_sales_products_tables.php
├── 2026_01_29_161626_add_partner_manager_commission_to_sales_products_table.php
├── 2026_01_29_162847_add_registration_fee_to_sales_products_table.php
├── 2026_03_14_100000_add_min_fees_to_sales_product_categories_table.php
└── 2026_03_14_100001_add_min_fees_to_sales_products_table.php
```
## 라우트
```php
// routes/web.php (sales/products prefix 그룹 내)
// 상품 관리
GET /products index() 메인 페이지
GET /products/list productList() 카테고리별 상품 목록 (HTMX)
POST /products store() 상품 등록
PUT /products/{id} update() 상품 수정
DELETE /products/{id} destroy() 상품 삭제
POST /products/{id}/toggle toggleActive() 활성화 토글
POST /products/reorder reorder() 순서 변경
// 카테고리 관리
GET /products/categories categories() 카테고리 목록
POST /products/categories storeCategory() 카테고리 생성
PUT /products/categories/{id} updateCategory() 카테고리 수정
DELETE /products/categories/{id} deleteCategory() 카테고리 삭제
// API (영업 시나리오용)
GET /products/api/list getProductsApi() 활성 상품 목록
```
## 컨트롤러
### SalesProductController
| 메서드 | HTTP | 설명 |
|--------|------|------|
| `index()` | GET | 메인 페이지 (HX-Redirect 처리) |
| `productList()` | GET | 카테고리별 상품 목록 (HTMX 파셜) |
| `store()` | POST | 상품 등록 (코드 중복 체크, 자동 순서) |
| `update()` | PUT | 상품 수정 (부분 업데이트 허용) |
| `destroy()` | DELETE | 상품 삭제 (Soft Delete) |
| `toggleActive()` | POST | 활성화/비활성화 토글 |
| `reorder()` | POST | 다중 상품 순서 변경 (배치) |
| `categories()` | GET | 활성 카테고리 목록 |
| `storeCategory()` | POST | 카테고리 생성 (코드 unique) |
| `updateCategory()` | PUT | 카테고리 수정 |
| `deleteCategory()` | DELETE | 카테고리 삭제 (하위 상품 있으면 실패) |
| `getProductsApi()` | GET | API: 활성 카테고리+상품 (영업 시나리오용) |
## 모델
### SalesProductCategory
**테이블**: `sales_product_categories`
| 필드 | 타입 | 설명 |
|------|------|------|
| `code` | string(50) | 카테고리 코드 (unique) |
| `name` | string(100) | 카테고리명 |
| `description` | text | 설명 |
| `base_storage` | string(20) | 기본 제공 용량 (기본: 100GB) |
| `min_development_fee` | decimal(15,2) | 최저 개발비 (카테고리 레벨, 기본 0) |
| `min_subscription_fee` | decimal(15,2) | 최저 구독료 (카테고리 레벨, 기본 0) |
| `display_order` | int | 표시 순서 |
| `is_active` | boolean | 활성화 |
- SoftDeletes 적용
- 관계: `products()`, `activeProducts()`
- Scope: `active()`, `ordered()`
### SalesProduct
**테이블**: `sales_products`
| 필드 | 타입 | 설명 |
|------|------|------|
| `category_id` | bigint (FK) | 카테고리 ID |
| `code` | string(50) | 상품 코드 (카테고리별 unique) |
| `name` | string(100) | 상품명 |
| `description` | text | 설명 (프로그램 타입) |
| `development_fee` | decimal(15,2) | 개발비 (원가) |
| `registration_fee` | decimal(15,2) | 개발비 (적용가) |
| `subscription_fee` | decimal(15,2) | 월 구독료 |
| `min_development_fee` | decimal(15,2) | 최저 개발비 (이 금액 이하 설정 불가, 기본 0) |
| `min_subscription_fee` | decimal(15,2) | 최저 구독료 (이 금액 이하 설정 불가, 기본 0) |
| `partner_commission_rate` | decimal(5,2) | 영업파트너 수당율 (기본 20%) |
| `manager_commission_rate` | decimal(5,2) | 매니저 수당율 (기본 5%) |
| `allow_flexible_pricing` | boolean | 재량권 허용 여부 |
| `is_required` | boolean | 필수 선택 여부 |
| `display_order` | int | 표시 순서 |
| `is_active` | boolean | 활성화 |
- SoftDeletes 적용
- Unique Key: `(category_id, code)`
- Scope: `active()`, `ordered()`
- Accessor: `total_commission_rate`, `commission`, `formatted_*_fee`
### SalesContractProduct (계약별 선택 상품)
**테이블**: `sales_contract_products`
| 필드 | 타입 | 설명 |
|------|------|------|
| `tenant_id` | bigint (FK, nullable) | 테넌트 ID |
| `management_id` | bigint (FK) | 영업관리 ID |
| `category_id` | bigint (FK) | 선택 카테고리 |
| `product_id` | bigint (FK) | 선택 상품 |
| `development_fee` | decimal(15,2) | 적용 개발비 |
| `registration_fee` | decimal(15,2) | 적용 개발비 |
| `subscription_fee` | decimal(15,2) | 적용 구독료 |
| `discount_rate` | decimal(5,2) | 할인율 (기본 0%) |
| `notes` | text | 비고 |
| `created_by` | bigint (FK) | 등록자 |
## 뷰 구성
### index.blade.php
```
┌─ 페이지 헤더 ──────────────────────
│ 제목: "상품관리"
│ [카테고리 관리] 버튼
├─ 카테고리 탭 ──────────────────────
│ [카테고리A] [카테고리B] [카테고리C] ...
│ └─ 선택 시 HTMX로 상품 목록 갱신
├─ 상품 영역 ────────────────────────
│ 헤더: 카테고리명 + 기본 용량 + [상품 추가]
│ ┌─ 상품 카드 (그리드: 1/2/3열 반응형) ─┐
│ │ 상품명 + 필수/비활성 배지 + 코드 │
│ │ 프로그램 설명 │
│ │ 개발비(원가/취소선) + 개발비(적용가) │
│ │ 월 구독료 │
│ │ 수당: 파트너 20%, 매니저 5% │
│ │ 재량권 허용/고정가 태그 + [삭제] │
│ └───────────────────────────────────────┘
├─ 상품 등록/수정 모달 (Alpine.js) ──
│ 코드, 상품명, 설명
│ 개발비(원가), 개발비(적용가), 구독료
│ ┌─ 최저가 설정 (빨간 박스) ────────┐
│ │ 최저 개발비, 최저 구독료 │
│ │ "절대 이 금액 이하로 내릴 수 없음"│
│ └──────────────────────────────────┘
│ 파트너 수당율, 매니저 수당율
│ 재량권 허용, 필수 선택
└─ 카테고리 관리 모달 ──────────────
코드, 카테고리명, 설명, 기본 용량
```
## 최저가 정책
상품별로 **최저 개발비**와 **최저 구독료**를 설정할 수 있다. 설정된 최저가 이하로는 절대 가격을 내릴 수 없다.
### 적용 범위
| 영역 | 최저 개발비 | 최저 구독료 |
|------|:----------:|:----------:|
| 상품 등록/수정 (MNG) | ✅ 서버 검증 | ✅ 서버 검증 |
| 가격 시뮬레이터 슬라이더 | ✅ 슬라이더 min 제한 | ✅ 연동 시 min 제한 |
| 프로모션 개발비 할인 | ✅ 할인 max 제한 | — |
| 프로모션 구독료 할인 | — | ✅ 할인 max 제한 |
| 개발비 전액 면제 | ✅ 최저가 설정 시 비활성화 | — |
### 검증 로직
- **컨트롤러**: `store()`, `update()``registration_fee >= min_development_fee`, `subscription_fee >= min_subscription_fee` 검증
- **시뮬레이터**: `setAdjustedFee()`에서 `Math.max(minDevFee, value)` 적용
- **프로모션**: `promoDevDiscountMax()`, `promoSubDiscountMaxPercent()`로 슬라이더 max 제한
---
## 가격 시뮬레이터 연동
**라우트**: `GET /sales/price-simulator`
가격 시뮬레이터는 상품관리의 데이터를 기반으로 실시간 비용/수당 시뮬레이션을 제공한다.
### 개발비-구독료 연동 조절
토글 스위치로 활성화하는 기능이다. 개발비 슬라이더를 조정하면 구독료가 원래 비율을 유지하며 자동 연동된다.
**비율 계산**:
```
ratio = 원래 구독료 / 원래 개발비(할인가)
연동 구독료 = 조정된 개발비 × ratio (만원 단위 반올림)
```
**예시** (개발비 2,000만원, 구독료 50만원인 상품):
| 조정된 개발비 | 비율 | 연동 구독료 |
|-------------:|:----:|----------:|
| 2,000만원 | 2.5% | 50만원 |
| 1,500만원 | 2.5% | 38만원 |
| 1,000만원 | 2.5% | 25만원 |
| 500만원 | 2.5% | 13만원 |
**제한사항**:
- 최저 구독료 이하로는 내려가지 않음
- 연동 OFF 시 구독료가 원래값으로 즉시 복원
- 프로모션 할인과 독립적으로 동작 (연동 → 프로모션 순서로 적용)
### 프로모션 할인과 최저가
프로모션 영역의 모든 할인 슬라이더는 최저가를 초과하지 않도록 max가 자동 제한된다.
| 프로모션 항목 | max 산정 기준 |
|-------------|-------------|
| 개발비 할인 (%) | `(적용가 합계 - 최저 개발비 합계) / 적용가 합계 × 100` |
| 개발비 할인 (원) | `적용가 합계 - 최저 개발비 합계` |
| 개발비 전액 면제 | 최저 개발비 > 0이면 체크박스 비활성화 |
| 구독료 할인 (%) | `(1 - 최저 구독료 합계 / 구독료 합계) × 100` |
---
## HTMX 호환성
- Alpine.js 스크립트가 `@push('scripts')`에 있어 **HX-Redirect 필요**
- 카테고리 탭 전환은 HTMX `hx-get`으로 부분 로드