From 7547a63284eeaf2fbdcada9aa543f0e9e8a440f8 Mon Sep 17 00:00:00 2001 From: pro Date: Thu, 29 Jan 2026 15:04:26 +0900 Subject: [PATCH] =?UTF-8?q?feat:=EA=B3=84=EC=95=BD=20=EC=B2=B4=EA=B2=B0=20?= =?UTF-8?q?=EB=8B=A8=EA=B3=84=EC=97=90=20=EC=83=81=ED=92=88=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SalesContractController: 계약 상품 저장/조회 API - product-selection.blade.php: 상품 선택 UI 컴포넌트 - scenario-step.blade.php: 6단계에서 상품 선택 컴포넌트 표시 - routes/web.php: /sales/contracts/* 라우트 추가 Co-Authored-By: Claude Opus 4.5 --- .../Sales/SalesContractController.php | 91 +++++++ .../partials/product-selection.blade.php | 237 ++++++++++++++++++ .../sales/modals/scenario-step.blade.php | 5 + routes/web.php | 6 + 4 files changed, 339 insertions(+) create mode 100644 app/Http/Controllers/Sales/SalesContractController.php create mode 100644 resources/views/sales/modals/partials/product-selection.blade.php diff --git a/app/Http/Controllers/Sales/SalesContractController.php b/app/Http/Controllers/Sales/SalesContractController.php new file mode 100644 index 00000000..acbb91c2 --- /dev/null +++ b/app/Http/Controllers/Sales/SalesContractController.php @@ -0,0 +1,91 @@ +validate([ + 'tenant_id' => 'required|exists:tenants,id', + 'products' => 'required|array', + 'products.*.product_id' => 'required|exists:sales_products,id', + 'products.*.category_id' => 'required|exists:sales_product_categories,id', + 'products.*.development_fee' => 'required|numeric|min:0', + 'products.*.subscription_fee' => 'required|numeric|min:0', + ]); + + try { + DB::transaction(function () use ($validated) { + $tenantId = $validated['tenant_id']; + + // 영업관리 레코드 조회 (없으면 생성) + $management = SalesTenantManagement::findOrCreateByTenant($tenantId); + + // 기존 상품 삭제 + SalesContractProduct::where('tenant_id', $tenantId)->delete(); + + // 새 상품 저장 + foreach ($validated['products'] as $product) { + SalesContractProduct::create([ + 'tenant_id' => $tenantId, + 'management_id' => $management->id, + 'category_id' => $product['category_id'], + 'product_id' => $product['product_id'], + 'development_fee' => $product['development_fee'], + 'subscription_fee' => $product['subscription_fee'], + 'discount_rate' => 0, + 'created_by' => auth()->id(), + ]); + } + }); + + return response()->json([ + 'success' => true, + 'message' => '계약 상품이 저장되었습니다.', + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => '저장 중 오류가 발생했습니다.', + ], 500); + } + } + + /** + * 계약 상품 조회 + */ + public function getProducts(int $tenantId): JsonResponse + { + $products = SalesContractProduct::where('tenant_id', $tenantId) + ->with(['product', 'category']) + ->get(); + + $totals = [ + 'development_fee' => $products->sum('development_fee'), + 'subscription_fee' => $products->sum('subscription_fee'), + 'count' => $products->count(), + ]; + + return response()->json([ + 'success' => true, + 'data' => [ + 'products' => $products, + 'totals' => $totals, + ], + ]); + } +} diff --git a/resources/views/sales/modals/partials/product-selection.blade.php b/resources/views/sales/modals/partials/product-selection.blade.php new file mode 100644 index 00000000..34328de1 --- /dev/null +++ b/resources/views/sales/modals/partials/product-selection.blade.php @@ -0,0 +1,237 @@ +{{-- 계약 체결 시 상품 선택 컴포넌트 --}} +@php + use App\Models\Sales\SalesProductCategory; + use App\Models\Sales\SalesContractProduct; + + $categories = SalesProductCategory::active() + ->ordered() + ->with(['products' => fn($q) => $q->active()->ordered()]) + ->get(); + + // 이미 선택된 상품들 조회 + $selectedProducts = SalesContractProduct::where('tenant_id', $tenant->id) + ->pluck('product_id') + ->toArray(); + + // 기존 계약 상품 정보 (가격 커스터마이징 포함) + $contractProducts = SalesContractProduct::where('tenant_id', $tenant->id) + ->get() + ->keyBy('product_id'); +@endphp + +
+
+
+ + + +
+
+

SAM 솔루션 상품 선택

+

고객사에 제공할 솔루션 패키지를 선택하세요

+
+
+ + {{-- 카테고리 탭 --}} +
+ @foreach($categories as $category) + + @endforeach +
+ + {{-- 상품 목록 --}} + @foreach($categories as $category) +
+
+ @foreach($category->products as $product) + @php + $isSelected = in_array($product->id, $selectedProducts); + $contractProduct = $contractProducts->get($product->id); + $devFee = $contractProduct?->development_fee ?? $product->development_fee; + $subFee = $contractProduct?->subscription_fee ?? $product->subscription_fee; + @endphp +
+
+
+ {{-- 체크박스 --}} + + +
+
+ {{ $product->name }} + @if($product->is_required) + 필수 + @endif + @if($product->allow_flexible_pricing) + 재량권 + @endif +
+ @if($product->description) +

{{ $product->description }}

+ @endif +
+ 가입비: {{ $product->formatted_development_fee }} + 월 구독료: {{ $product->formatted_subscription_fee }} +
+
+
+
+
+ @endforeach +
+
+ @endforeach + + {{-- 합계 영역 --}} +
+
+
+
+

선택 상품

+

+
+
+

총 가입비

+

+
+
+

월 구독료

+

+
+
+
+
+ + {{-- 저장 버튼 --}} +
+ +
+
+ + diff --git a/resources/views/sales/modals/scenario-step.blade.php b/resources/views/sales/modals/scenario-step.blade.php index 578720d1..dee0df34 100644 --- a/resources/views/sales/modals/scenario-step.blade.php +++ b/resources/views/sales/modals/scenario-step.blade.php @@ -149,6 +149,11 @@ class="border-t border-gray-100"> @endforeach + {{-- 계약 체결 단계 (Step 6)에서만 상품 선택 컴포넌트 표시 --}} + @if($step['id'] === 6 && $scenarioType === 'sales') + @include('sales.modals.partials.product-selection', ['tenant' => $tenant]) + @endif + {{-- 단계 이동 버튼 --}} @php $currentStepId = (int) $step['id']; diff --git a/routes/web.php b/routes/web.php index 9d496683..55e20518 100644 --- a/routes/web.php +++ b/routes/web.php @@ -842,4 +842,10 @@ // API (영업 시나리오용) Route::get('/api/list', [SalesProductController::class, 'getProductsApi'])->name('api.list'); }); + + // 계약관리 + Route::prefix('contracts')->name('contracts.')->group(function () { + Route::post('/products', [\App\Http\Controllers\Sales\SalesContractController::class, 'saveProducts'])->name('products.save'); + Route::get('/products/{tenant}', [\App\Http\Controllers\Sales\SalesContractController::class, 'getProducts'])->name('products.get'); + }); });