Files
sam-manage/app/Http/Controllers/Sales/SalesProductController.php
김보곤 bd81eebf07 feat: [상품관리] 카테고리별 최저 개발비/최저 구독료 설정 기능 추가
- 카테고리 관리에서 최저 개발비, 최저 구독료 설정 가능
- 상품 추가/수정 시 최저가 이하 입력 차단 (서버 검증)
- 상품 목록에 최저가 안내 배너 표시 (경고 아이콘)
- 상품 모달에서 실시간 최저가 미달 경고 표시 (빨간 테두리)
2026-03-14 14:43:03 +09:00

321 lines
10 KiB
PHP

<?php
namespace App\Http\Controllers\Sales;
use App\Http\Controllers\Controller;
use App\Models\Sales\SalesProduct;
use App\Models\Sales\SalesProductCategory;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\View\View;
/**
* 영업 상품관리 컨트롤러 (HQ 전용)
*/
class SalesProductController extends Controller
{
/**
* 상품관리 메인 화면
*/
public function index(Request $request): View|Response
{
// HTMX 요청인 경우 전체 페이지 리로드 (Alpine.js 스크립트 실행 필요)
if ($request->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('sales.products.index'));
}
$categories = SalesProductCategory::active()
->ordered()
->with(['products' => fn ($q) => $q->ordered()])
->get();
$currentCategoryCode = $request->input('category', $categories->first()?->code);
$currentCategory = $categories->firstWhere('code', $currentCategoryCode) ?? $categories->first();
return view('sales.products.index', compact('categories', 'currentCategory'));
}
/**
* 상품 목록 (HTMX용)
*/
public function productList(Request $request): View
{
$categoryCode = $request->input('category');
$category = SalesProductCategory::where('code', $categoryCode)
->with(['products' => fn ($q) => $q->ordered()])
->first();
return view('sales.products.partials.product-list', compact('category'));
}
/**
* 상품 저장
*/
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'category_id' => 'required|exists:sales_product_categories,id',
'code' => 'required|string|max:50',
'name' => 'required|string|max:100',
'description' => 'nullable|string',
'development_fee' => 'required|numeric|min:0',
'registration_fee' => 'required|numeric|min:0',
'subscription_fee' => 'required|numeric|min:0',
'partner_commission_rate' => 'nullable|numeric|min:0|max:100',
'manager_commission_rate' => 'nullable|numeric|min:0|max:100',
'allow_flexible_pricing' => 'boolean',
'is_required' => 'boolean',
]);
// 최저가 검증
$category = SalesProductCategory::findOrFail($validated['category_id']);
$minFeeErrors = $this->validateMinFees($category, $validated);
if ($minFeeErrors) {
return response()->json(['success' => false, 'message' => $minFeeErrors], 422);
}
// 코드 중복 체크
$exists = SalesProduct::where('category_id', $validated['category_id'])
->where('code', $validated['code'])
->exists();
if ($exists) {
return response()->json([
'success' => false,
'message' => '이미 존재하는 상품 코드입니다.',
], 422);
}
// 순서 설정 (마지막)
$maxOrder = SalesProduct::where('category_id', $validated['category_id'])->max('display_order') ?? 0;
$validated['display_order'] = $maxOrder + 1;
$validated['partner_commission_rate'] = $validated['partner_commission_rate'] ?? 20.00;
$validated['manager_commission_rate'] = $validated['manager_commission_rate'] ?? 5.00;
$product = SalesProduct::create($validated);
return response()->json([
'success' => true,
'message' => '상품이 등록되었습니다.',
'product' => $product,
]);
}
/**
* 상품 수정
*/
public function update(Request $request, int $id): JsonResponse
{
$product = SalesProduct::findOrFail($id);
$validated = $request->validate([
'name' => 'sometimes|string|max:100',
'description' => 'nullable|string',
'development_fee' => 'sometimes|numeric|min:0',
'registration_fee' => 'sometimes|numeric|min:0',
'subscription_fee' => 'sometimes|numeric|min:0',
'partner_commission_rate' => 'nullable|numeric|min:0|max:100',
'manager_commission_rate' => 'nullable|numeric|min:0|max:100',
'allow_flexible_pricing' => 'boolean',
'is_required' => 'boolean',
'is_active' => 'boolean',
]);
// 최저가 검증
$category = $product->category;
$checkData = array_merge($product->toArray(), $validated);
$minFeeErrors = $this->validateMinFees($category, $checkData);
if ($minFeeErrors) {
return response()->json(['success' => false, 'message' => $minFeeErrors], 422);
}
$product->update($validated);
return response()->json([
'success' => true,
'message' => '상품이 수정되었습니다.',
'product' => $product->fresh(),
]);
}
/**
* 상품 삭제 (soft delete)
*/
public function destroy(int $id): JsonResponse
{
$product = SalesProduct::findOrFail($id);
$product->delete();
return response()->json([
'success' => true,
'message' => '상품이 삭제되었습니다.',
]);
}
/**
* 활성화 토글
*/
public function toggleActive(int $id): JsonResponse
{
$product = SalesProduct::findOrFail($id);
$product->update(['is_active' => ! $product->is_active]);
return response()->json([
'success' => true,
'message' => $product->is_active ? '상품이 활성화되었습니다.' : '상품이 비활성화되었습니다.',
'is_active' => $product->is_active,
]);
}
/**
* 순서 변경
*/
public function reorder(Request $request): JsonResponse
{
$validated = $request->validate([
'orders' => 'required|array',
'orders.*.id' => 'required|exists:sales_products,id',
'orders.*.order' => 'required|integer|min:0',
]);
foreach ($validated['orders'] as $item) {
SalesProduct::where('id', $item['id'])->update(['display_order' => $item['order']]);
}
return response()->json([
'success' => true,
'message' => '순서가 변경되었습니다.',
]);
}
// ==================== 카테고리 관리 ====================
/**
* 카테고리 목록
*/
public function categories(): JsonResponse
{
$categories = SalesProductCategory::ordered()->get();
return response()->json([
'success' => true,
'data' => $categories,
]);
}
/**
* 카테고리 생성
*/
public function storeCategory(Request $request): JsonResponse
{
$validated = $request->validate([
'code' => 'required|string|max:50|unique:sales_product_categories,code',
'name' => 'required|string|max:100',
'description' => 'nullable|string',
'base_storage' => 'nullable|string|max:20',
]);
$maxOrder = SalesProductCategory::max('display_order') ?? 0;
$validated['display_order'] = $maxOrder + 1;
$category = SalesProductCategory::create($validated);
return response()->json([
'success' => true,
'message' => '카테고리가 생성되었습니다.',
'category' => $category,
]);
}
/**
* 카테고리 수정
*/
public function updateCategory(Request $request, int $id): JsonResponse
{
$category = SalesProductCategory::findOrFail($id);
$validated = $request->validate([
'name' => 'sometimes|string|max:100',
'description' => 'nullable|string',
'base_storage' => 'nullable|string|max:20',
'min_development_fee' => 'nullable|numeric|min:0',
'min_subscription_fee' => 'nullable|numeric|min:0',
'is_active' => 'boolean',
]);
$category->update($validated);
return response()->json([
'success' => true,
'message' => '카테고리가 수정되었습니다.',
'category' => $category->fresh(),
]);
}
/**
* 카테고리 삭제
*/
public function deleteCategory(int $id): JsonResponse
{
$category = SalesProductCategory::findOrFail($id);
// 상품이 있으면 삭제 불가
if ($category->products()->exists()) {
return response()->json([
'success' => false,
'message' => '상품이 있는 카테고리는 삭제할 수 없습니다.',
], 422);
}
$category->delete();
return response()->json([
'success' => true,
'message' => '카테고리가 삭제되었습니다.',
]);
}
// ==================== 내부 헬퍼 ====================
/**
* 최저가 검증
*/
private function validateMinFees(SalesProductCategory $category, array $data): ?string
{
$errors = [];
if ($category->min_development_fee > 0 && isset($data['registration_fee'])) {
if ($data['registration_fee'] < $category->min_development_fee) {
$errors[] = '개발비(할인가)는 최저 개발비 ₩'.number_format($category->min_development_fee).' 이상이어야 합니다.';
}
}
if ($category->min_subscription_fee > 0 && isset($data['subscription_fee'])) {
if ($data['subscription_fee'] < $category->min_subscription_fee) {
$errors[] = '월 구독료는 최저 구독료 ₩'.number_format($category->min_subscription_fee).' 이상이어야 합니다.';
}
}
return $errors ? implode(' ', $errors) : null;
}
// ==================== API (영업 시나리오용) ====================
/**
* 상품 목록 API (카테고리 포함)
*/
public function getProductsApi(): JsonResponse
{
$categories = SalesProductCategory::active()
->ordered()
->with(['products' => fn ($q) => $q->active()->ordered()])
->get();
return response()->json([
'success' => true,
'data' => $categories,
]);
}
}