diff --git a/app/Http/Controllers/Api/Admin/Rd/AiQuotationController.php b/app/Http/Controllers/Api/Admin/Rd/AiQuotationController.php new file mode 100644 index 00000000..4add3da7 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/Rd/AiQuotationController.php @@ -0,0 +1,109 @@ +only(['status', 'search', 'per_page']); + $quotations = $this->quotationService->getList($params); + + if ($request->header('HX-Request')) { + return view('rd.ai-quotation.partials.table', compact('quotations')); + } + + return response()->json([ + 'success' => true, + 'data' => $quotations, + ]); + } + + /** + * 견적 생성 + AI 분석 실행 + */ + public function store(StoreAiQuotationRequest $request): JsonResponse + { + $result = $this->quotationService->createAndAnalyze($request->validated()); + + if ($result['ok']) { + return response()->json([ + 'success' => true, + 'message' => 'AI 분석이 완료되었습니다.', + 'data' => $result['quotation'], + ]); + } + + return response()->json([ + 'success' => false, + 'message' => 'AI 분석에 실패했습니다.', + 'error' => $result['error'], + 'data' => $result['quotation'] ?? null, + ], 422); + } + + /** + * 상세 조회 + */ + public function show(int $id): JsonResponse + { + $quotation = $this->quotationService->getById($id); + + if (! $quotation) { + return response()->json([ + 'success' => false, + 'message' => 'AI 견적을 찾을 수 없습니다.', + ], 404); + } + + return response()->json([ + 'success' => true, + 'data' => $quotation, + ]); + } + + /** + * AI 재분석 + */ + public function analyze(int $id): JsonResponse + { + $quotation = $this->quotationService->getById($id); + + if (! $quotation) { + return response()->json([ + 'success' => false, + 'message' => 'AI 견적을 찾을 수 없습니다.', + ], 404); + } + + $result = $this->quotationService->runAnalysis($quotation); + + if ($result['ok']) { + return response()->json([ + 'success' => true, + 'message' => 'AI 재분석이 완료되었습니다.', + 'data' => $result['quotation'], + ]); + } + + return response()->json([ + 'success' => false, + 'message' => 'AI 재분석에 실패했습니다.', + 'error' => $result['error'], + ], 422); + } +} diff --git a/app/Http/Controllers/RdController.php b/app/Http/Controllers/RdController.php new file mode 100644 index 00000000..9ca2b89b --- /dev/null +++ b/app/Http/Controllers/RdController.php @@ -0,0 +1,74 @@ +header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('rd.index')); + } + + $dashboard = $this->quotationService->getDashboardStats(); + $statuses = AiQuotation::getStatuses(); + + return view('rd.index', compact('dashboard', 'statuses')); + } + + /** + * AI 견적 목록 + */ + public function quotations(Request $request): View|\Illuminate\Http\Response + { + if ($request->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('rd.ai-quotation.index')); + } + + $statuses = AiQuotation::getStatuses(); + + return view('rd.ai-quotation.index', compact('statuses')); + } + + /** + * AI 견적 생성 폼 + */ + public function createQuotation(Request $request): View|\Illuminate\Http\Response + { + if ($request->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('rd.ai-quotation.create')); + } + + return view('rd.ai-quotation.create'); + } + + /** + * AI 견적 상세 + */ + public function showQuotation(Request $request, int $id): View|\Illuminate\Http\Response + { + if ($request->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('rd.ai-quotation.show', $id)); + } + + $quotation = $this->quotationService->getById($id); + + if (! $quotation) { + abort(404, 'AI 견적을 찾을 수 없습니다.'); + } + + return view('rd.ai-quotation.show', compact('quotation')); + } +} diff --git a/app/Http/Requests/Rd/StoreAiQuotationRequest.php b/app/Http/Requests/Rd/StoreAiQuotationRequest.php new file mode 100644 index 00000000..aad91373 --- /dev/null +++ b/app/Http/Requests/Rd/StoreAiQuotationRequest.php @@ -0,0 +1,33 @@ + 'required|string|max:200', + 'input_type' => 'required|in:text,voice,document', + 'input_text' => 'required_if:input_type,text|nullable|string', + 'ai_provider' => 'nullable|in:gemini,claude', + ]; + } + + public function messages(): array + { + return [ + 'title.required' => '견적 제목을 입력하세요.', + 'title.max' => '제목은 200자 이내로 입력하세요.', + 'input_type.required' => '입력 유형을 선택하세요.', + 'input_text.required_if' => '인터뷰 내용을 입력하세요.', + ]; + } +} diff --git a/app/Models/Rd/AiQuotation.php b/app/Models/Rd/AiQuotation.php new file mode 100644 index 00000000..44c03e81 --- /dev/null +++ b/app/Models/Rd/AiQuotation.php @@ -0,0 +1,130 @@ + 'array', + 'quotation_result' => 'array', + 'options' => 'array', + 'total_dev_cost' => 'decimal:0', + 'total_monthly_fee' => 'decimal:0', + ]; + + public const STATUS_PENDING = 'pending'; + + public const STATUS_PROCESSING = 'processing'; + + public const STATUS_COMPLETED = 'completed'; + + public const STATUS_FAILED = 'failed'; + + public static function getStatuses(): array + { + return [ + self::STATUS_PENDING => '대기', + self::STATUS_PROCESSING => '분석중', + self::STATUS_COMPLETED => '완료', + self::STATUS_FAILED => '실패', + ]; + } + + public function getStatusLabelAttribute(): string + { + return match ($this->status) { + self::STATUS_PENDING => '대기', + self::STATUS_PROCESSING => '분석중', + self::STATUS_COMPLETED => '완료', + self::STATUS_FAILED => '실패', + default => $this->status, + }; + } + + public function getStatusColorAttribute(): string + { + return match ($this->status) { + self::STATUS_PENDING => 'badge-warning', + self::STATUS_PROCESSING => 'badge-info', + self::STATUS_COMPLETED => 'badge-success', + self::STATUS_FAILED => 'badge-error', + default => 'badge-ghost', + }; + } + + public function isCompleted(): bool + { + return $this->status === self::STATUS_COMPLETED; + } + + public function isProcessing(): bool + { + return $this->status === self::STATUS_PROCESSING; + } + + // Relations + + public function items(): HasMany + { + return $this->hasMany(AiQuotationItem::class, 'ai_quotation_id')->orderBy('sort_order'); + } + + public function creator(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by'); + } + + // Helpers + + public function getOption(string $key, $default = null) + { + return data_get($this->options, $key, $default); + } + + public function setOption(string $key, $value): void + { + $options = $this->options ?? []; + data_set($options, $key, $value); + $this->options = $options; + $this->save(); + } + + public function getFormattedDevCostAttribute(): string + { + return number_format((int) $this->total_dev_cost).'원'; + } + + public function getFormattedMonthlyFeeAttribute(): string + { + return number_format((int) $this->total_monthly_fee).'원'; + } +} diff --git a/app/Models/Rd/AiQuotationItem.php b/app/Models/Rd/AiQuotationItem.php new file mode 100644 index 00000000..57b16c94 --- /dev/null +++ b/app/Models/Rd/AiQuotationItem.php @@ -0,0 +1,64 @@ + 'boolean', + 'options' => 'array', + 'dev_cost' => 'decimal:0', + 'monthly_fee' => 'decimal:0', + ]; + + public function quotation(): BelongsTo + { + return $this->belongsTo(AiQuotation::class, 'ai_quotation_id'); + } + + public function module(): BelongsTo + { + return $this->belongsTo(AiQuotationModule::class, 'module_id'); + } + + public function getFormattedDevCostAttribute(): string + { + return number_format((int) $this->dev_cost).'원'; + } + + public function getFormattedMonthlyFeeAttribute(): string + { + return number_format((int) $this->monthly_fee).'원'; + } + + public function getOption(string $key, $default = null) + { + return data_get($this->options, $key, $default); + } + + public function setOption(string $key, $value): void + { + $options = $this->options ?? []; + data_set($options, $key, $value); + $this->options = $options; + $this->save(); + } +} diff --git a/app/Models/Rd/AiQuotationModule.php b/app/Models/Rd/AiQuotationModule.php new file mode 100644 index 00000000..5378f21d --- /dev/null +++ b/app/Models/Rd/AiQuotationModule.php @@ -0,0 +1,88 @@ + 'array', + 'options' => 'array', + 'is_active' => 'boolean', + 'dev_cost' => 'decimal:0', + 'monthly_fee' => 'decimal:0', + ]; + + public const CATEGORY_BASIC = 'basic'; + + public const CATEGORY_INDIVIDUAL = 'individual'; + + public const CATEGORY_ADDON = 'addon'; + + public static function getCategories(): array + { + return [ + self::CATEGORY_BASIC => '기본 패키지', + self::CATEGORY_INDIVIDUAL => '개별 모듈', + self::CATEGORY_ADDON => '부가 옵션', + ]; + } + + public function scopeActive($query) + { + return $query->where('is_active', true); + } + + /** + * AI 프롬프트에 주입할 활성 모듈 목록 조회 + */ + public static function getActiveModulesForPrompt(): array + { + return self::active() + ->orderBy('sort_order') + ->get() + ->map(fn ($m) => [ + 'module_code' => $m->module_code, + 'module_name' => $m->module_name, + 'category' => $m->category, + 'description' => $m->description, + 'keywords' => $m->keywords, + 'dev_cost' => (int) $m->dev_cost, + 'monthly_fee' => (int) $m->monthly_fee, + ]) + ->toArray(); + } + + public function getOption(string $key, $default = null) + { + return data_get($this->options, $key, $default); + } + + public function setOption(string $key, $value): void + { + $options = $this->options ?? []; + data_set($options, $key, $value); + $this->options = $options; + $this->save(); + } +} diff --git a/app/Services/Rd/AiQuotationService.php b/app/Services/Rd/AiQuotationService.php new file mode 100644 index 00000000..71a85e01 --- /dev/null +++ b/app/Services/Rd/AiQuotationService.php @@ -0,0 +1,434 @@ +with('creator:id,name') + ->orderBy('created_at', 'desc'); + + if (! empty($params['status'])) { + $query->where('status', $params['status']); + } + + if (! empty($params['search'])) { + $search = $params['search']; + $query->where(function ($q) use ($search) { + $q->where('title', 'like', "%{$search}%") + ->orWhere('input_text', 'like', "%{$search}%"); + }); + } + + return $query->paginate($params['per_page'] ?? 10); + } + + /** + * 상세 조회 + */ + public function getById(int $id): ?AiQuotation + { + return AiQuotation::with(['items', 'creator:id,name'])->find($id); + } + + /** + * 견적 요청 생성 + AI 분석 실행 + */ + public function createAndAnalyze(array $data): array + { + $provider = $data['ai_provider'] ?? 'gemini'; + + $quotation = AiQuotation::create([ + 'tenant_id' => session('selected_tenant_id', 1), + 'title' => $data['title'], + 'input_type' => $data['input_type'] ?? 'text', + 'input_text' => $data['input_text'] ?? null, + 'ai_provider' => $provider, + 'status' => AiQuotation::STATUS_PENDING, + 'created_by' => Auth::id(), + ]); + + return $this->runAnalysis($quotation); + } + + /** + * AI 분석 실행 (재분석 가능) + */ + public function runAnalysis(AiQuotation $quotation): array + { + try { + $quotation->update(['status' => AiQuotation::STATUS_PROCESSING]); + + $provider = $quotation->ai_provider; + $config = AiConfig::getActive($provider); + + if (! $config) { + throw new \RuntimeException("{$provider} API 설정이 없습니다."); + } + + // 1차 호출: 업무 분석 + $modules = AiQuotationModule::getActiveModulesForPrompt(); + $analysisPrompt = $this->buildAnalysisPrompt($quotation->input_text, $modules); + $analysisRaw = $this->callAi($config, $provider, $analysisPrompt, 'AI견적-업무분석'); + + $analysisResult = $this->parseJsonResponse($analysisRaw); + if (! $analysisResult) { + throw new \RuntimeException('AI 업무 분석 결과 파싱 실패'); + } + + $quotation->update(['analysis_result' => $analysisResult]); + + // 2차 호출: 견적 생성 + $quotationPrompt = $this->buildQuotationPrompt($analysisResult, $modules); + $quotationRaw = $this->callAi($config, $provider, $quotationPrompt, 'AI견적-견적생성'); + + $quotationResult = $this->parseJsonResponse($quotationRaw); + if (! $quotationResult) { + throw new \RuntimeException('AI 견적 생성 결과 파싱 실패'); + } + + // 추천 모듈 아이템 저장 + $this->saveQuotationItems($quotation, $quotationResult, $modules); + + // 합계 계산 + $totals = $quotation->items()->selectRaw( + 'SUM(dev_cost) as total_dev, SUM(monthly_fee) as total_monthly' + )->first(); + + $quotation->update([ + 'quotation_result' => $quotationResult, + 'ai_model' => $config->model, + 'total_dev_cost' => $totals->total_dev ?? 0, + 'total_monthly_fee' => $totals->total_monthly ?? 0, + 'status' => AiQuotation::STATUS_COMPLETED, + ]); + + return [ + 'ok' => true, + 'quotation' => $quotation->fresh(['items', 'creator:id,name']), + ]; + } catch (\Exception $e) { + Log::error('AI 견적 분석 실패', [ + 'quotation_id' => $quotation->id, + 'error' => $e->getMessage(), + ]); + + $quotation->update(['status' => AiQuotation::STATUS_FAILED]); + + return [ + 'ok' => false, + 'error' => $e->getMessage(), + 'quotation' => $quotation->fresh(), + ]; + } + } + + /** + * AI API 호출 (Gemini / Claude) + */ + private function callAi(AiConfig $config, string $provider, string $prompt, string $menuName): ?string + { + return match ($provider) { + 'gemini' => $this->callGemini($config, $prompt, $menuName), + 'claude' => $this->callClaude($config, $prompt, $menuName), + default => throw new \RuntimeException("지원하지 않는 AI Provider: {$provider}"), + }; + } + + /** + * Gemini API 호출 + */ + private function callGemini(AiConfig $config, string $prompt, string $menuName): ?string + { + $model = $config->model; + $apiKey = $config->api_key; + $baseUrl = $config->base_url ?? 'https://generativelanguage.googleapis.com/v1beta'; + + $url = "{$baseUrl}/models/{$model}:generateContent?key={$apiKey}"; + + $response = Http::timeout(120) + ->withHeaders(['Content-Type' => 'application/json']) + ->post($url, [ + 'contents' => [ + ['parts' => [['text' => $prompt]]], + ], + 'generationConfig' => [ + 'temperature' => 0.3, + 'maxOutputTokens' => 8192, + 'responseMimeType' => 'application/json', + ], + ]); + + if (! $response->successful()) { + Log::error('Gemini API error', [ + 'status' => $response->status(), + 'body' => $response->body(), + ]); + throw new \RuntimeException('Gemini API 호출 실패: '.$response->status()); + } + + $result = $response->json(); + + AiTokenHelper::saveGeminiUsage($result, $result['modelVersion'] ?? $model, $menuName); + + return $result['candidates'][0]['content']['parts'][0]['text'] ?? null; + } + + /** + * Claude API 호출 + */ + private function callClaude(AiConfig $config, string $prompt, string $menuName): ?string + { + $response = Http::timeout(120) + ->withHeaders([ + 'x-api-key' => $config->api_key, + 'anthropic-version' => '2023-06-01', + 'content-type' => 'application/json', + ]) + ->post($config->base_url.'/messages', [ + 'model' => $config->model, + 'max_tokens' => 8192, + 'temperature' => 0.3, + 'messages' => [ + ['role' => 'user', 'content' => $prompt], + ], + ]); + + if (! $response->successful()) { + Log::error('Claude API error', [ + 'status' => $response->status(), + 'body' => $response->body(), + ]); + throw new \RuntimeException('Claude API 호출 실패: '.$response->status()); + } + + $result = $response->json(); + + AiTokenHelper::saveClaudeUsage($result, $config->model, $menuName); + + return $result['content'][0]['text'] ?? null; + } + + /** + * 1차 프롬프트: 업무 분석 + */ + private function buildAnalysisPrompt(string $interviewText, array $modules): string + { + $modulesJson = json_encode($modules, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + + return << json_last_error_msg(), + 'response_preview' => mb_substr($response, 0, 500), + ]); + + return null; + } + + return $decoded; + } + + /** + * AI 견적 결과를 아이템으로 저장 + */ + private function saveQuotationItems(AiQuotation $quotation, array $quotationResult, array $modules): void + { + // 기존 아이템 삭제 (재분석 시) + $quotation->items()->delete(); + + $items = $quotationResult['items'] ?? []; + $moduleMap = collect($modules)->keyBy('module_code'); + + foreach ($items as $index => $item) { + $moduleCode = $item['module_code'] ?? ''; + $catalogModule = $moduleMap->get($moduleCode); + + // 카탈로그에 있는 모듈이면 DB의 가격 사용 (AI hallucination 방지) + $devCost = $catalogModule ? $catalogModule['dev_cost'] : ($item['dev_cost'] ?? 0); + $monthlyFee = $catalogModule ? $catalogModule['monthly_fee'] : ($item['monthly_fee'] ?? 0); + + // DB에서 module_id 조회 + $dbModule = AiQuotationModule::where('module_code', $moduleCode)->first(); + + AiQuotationItem::create([ + 'ai_quotation_id' => $quotation->id, + 'module_id' => $dbModule?->id, + 'module_code' => $moduleCode, + 'module_name' => $item['module_name'] ?? $moduleCode, + 'is_required' => $item['is_required'] ?? false, + 'reason' => $item['reason'] ?? null, + 'dev_cost' => $devCost, + 'monthly_fee' => $monthlyFee, + 'sort_order' => $index, + ]); + } + } + + /** + * 대시보드 통계 + */ + public function getDashboardStats(): array + { + $stats = AiQuotation::query() + ->selectRaw(" + COUNT(*) as total, + SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed, + SUM(CASE WHEN status = 'processing' THEN 1 ELSE 0 END) as processing, + SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed, + SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending + ") + ->first(); + + $recent = AiQuotation::with('creator:id,name') + ->orderBy('created_at', 'desc') + ->limit(5) + ->get(); + + return [ + 'stats' => [ + 'total' => (int) $stats->total, + 'completed' => (int) $stats->completed, + 'processing' => (int) $stats->processing, + 'failed' => (int) $stats->failed, + 'pending' => (int) $stats->pending, + ], + 'recent' => $recent, + ]; + } +} diff --git a/resources/views/rd/ai-quotation/create.blade.php b/resources/views/rd/ai-quotation/create.blade.php new file mode 100644 index 00000000..2ee12633 --- /dev/null +++ b/resources/views/rd/ai-quotation/create.blade.php @@ -0,0 +1,174 @@ +@extends('layouts.app') + +@section('title', 'AI 견적서 생성') + +@section('content') + +
+

+ + AI 견적서 생성 +

+ + 목록으로 + +
+ + +
+
+

인터뷰 내용 입력

+

고객사 인터뷰 내용을 입력하면 AI가 업무를 분석하고 맞춤형 견적서를 자동 생성합니다.

+
+ +
+ +
+ + +
+ + +
+ +
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+ + +

인터뷰 내용이 구체적일수록 정확한 견적이 생성됩니다.

+
+ + +
+ + 취소 + + +
+
+
+ + + +@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/rd/ai-quotation/index.blade.php b/resources/views/rd/ai-quotation/index.blade.php new file mode 100644 index 00000000..aa33df83 --- /dev/null +++ b/resources/views/rd/ai-quotation/index.blade.php @@ -0,0 +1,86 @@ +@extends('layouts.app') + +@section('title', 'AI 견적 엔진') + +@section('content') + +
+

+ + AI 견적 엔진 +

+ +
+ + +
+
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
+
+ +

목록을 불러오는 중...

+
+
+@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/rd/ai-quotation/partials/table.blade.php b/resources/views/rd/ai-quotation/partials/table.blade.php new file mode 100644 index 00000000..db47f557 --- /dev/null +++ b/resources/views/rd/ai-quotation/partials/table.blade.php @@ -0,0 +1,59 @@ +
+ + + + + + + + + + + + + + + @forelse($quotations as $q) + + + + + + + + + + + @empty + + + + @endforelse + +
ID제목상태AI개발비월 구독료요청자생성일
#{{ $q->id }}{{ Str::limit($q->title, 40) }} + {{ $q->status_label }} + + {{ $q->ai_provider }} + + @if($q->isCompleted()) + {{ number_format((int)$q->total_dev_cost) }}원 + @else + - + @endif + + @if($q->isCompleted()) + {{ number_format((int)$q->total_monthly_fee) }}원/월 + @else + - + @endif + {{ $q->creator?->name ?? '-' }}{{ $q->created_at->format('m/d H:i') }}
+ +

AI 견적 데이터가 없습니다.

+
+ + @if($quotations->hasPages()) +
+ {{ $quotations->links() }} +
+ @endif +
diff --git a/resources/views/rd/ai-quotation/show.blade.php b/resources/views/rd/ai-quotation/show.blade.php new file mode 100644 index 00000000..f647f1c5 --- /dev/null +++ b/resources/views/rd/ai-quotation/show.blade.php @@ -0,0 +1,296 @@ +@extends('layouts.app') + +@section('title', 'AI 견적 상세') + +@section('content') + +
+
+

+ + {{ $quotation->title }} +

+ {{ $quotation->status_label }} +
+
+ + 목록 + + @if($quotation->isCompleted() || $quotation->status === 'failed') + + @endif +
+
+ + +
+
+
+

AI Provider

+

{{ strtoupper($quotation->ai_provider) }}{{ $quotation->ai_model ? ' ('.$quotation->ai_model.')' : '' }}

+
+
+

입력 유형

+

{{ ['text' => '텍스트', 'voice' => '음성', 'document' => '문서'][$quotation->input_type] ?? $quotation->input_type }}

+
+
+

요청자

+

{{ $quotation->creator?->name ?? '-' }}

+
+
+

생성일

+

{{ $quotation->created_at->format('Y-m-d H:i') }}

+
+
+
+ +@if($quotation->isCompleted()) + + @if($quotation->analysis_result) + @php $analysis = $quotation->analysis_result; @endphp +
+
+

+ AI 업무 분석 결과 +

+
+
+ + @if(isset($analysis['company_analysis'])) + @php $company = $analysis['company_analysis']; @endphp +
+
+ 업종 + {{ $company['industry'] ?? '-' }} +
+
+ 규모 + {{ $company['scale'] ?? '-' }} +
+
+ 디지털화 수준 + {{ $company['digitalization_level'] ?? '-' }} +
+ @if(!empty($company['current_systems'])) +
+ 현재 시스템 + {{ implode(', ', $company['current_systems']) }} +
+ @endif +
+ @endif + + + @if(!empty($analysis['business_domains'])) +

업무 영역 분석

+
+ @foreach($analysis['business_domains'] as $domain) +
+
+

{{ $domain['domain'] ?? '' }}

+ @php + $priorityColor = match($domain['priority'] ?? '') { + '필수' => 'bg-red-100 text-red-700', + '높음' => 'bg-orange-100 text-orange-700', + '보통' => 'bg-yellow-100 text-yellow-700', + default => 'bg-gray-100 text-gray-700', + }; + @endphp + {{ $domain['priority'] ?? '' }} +
+

{{ $domain['current_process'] ?? '' }}

+ @if(!empty($domain['pain_points'])) +
+ @foreach($domain['pain_points'] as $point) + {{ $point }} + @endforeach +
+ @endif + @if(!empty($domain['matched_modules'])) +
+ @foreach($domain['matched_modules'] as $mod) + {{ $mod }} + @endforeach +
+ @endif +
+ @endforeach +
+ @endif +
+
+ @endif + + +
+
+

+ 추천 모듈 및 견적 +

+
+
+ + + + + + + + + + + + @foreach($quotation->items as $item) + + + + + + + + @endforeach + + + + + + + + +
구분모듈추천 근거개발비월 구독료
+ @if($item->is_required) + 필수 + @else + 선택 + @endif + +
{{ $item->module_name }}
+
{{ $item->module_code }}
+
+ {{ Str::limit($item->reason, 100) }} + {{ number_format((int)$item->dev_cost) }}원{{ number_format((int)$item->monthly_fee) }}원
합계{{ number_format((int)$quotation->total_dev_cost) }}원{{ number_format((int)$quotation->total_monthly_fee) }}원/월
+
+
+ + + @if(!empty($quotation->quotation_result['implementation_plan'])) + @php $plan = $quotation->quotation_result['implementation_plan']; @endphp +
+
+

+ 구현 계획 (AI 추천) +

+
+
+

예상 기간: {{ $plan['estimated_months'] ?? '?' }}개월

+ @if(!empty($plan['phases'])) +
+ @foreach($plan['phases'] as $phase) +
+
+ {{ $phase['phase'] ?? '' }} +
+
+

{{ $phase['name'] ?? '' }}

+
+ {{ $phase['duration_weeks'] ?? '?' }}주 + @if(!empty($phase['modules'])) + | + @foreach($phase['modules'] as $mod) + {{ $mod }} + @endforeach + @endif +
+
+
+ @endforeach +
+ @endif +
+
+ @endif + + + @if(!empty($quotation->quotation_result['analysis_summary'])) +
+

+ AI 분석 요약 +

+

{{ $quotation->quotation_result['analysis_summary'] }}

+
+ @endif + +@elseif($quotation->status === 'failed') + +
+
+ +

AI 분석 실패

+
+

AI 분석 중 오류가 발생했습니다. 다시 시도하거나 입력 내용을 수정해 주세요.

+
+ +@elseif($quotation->isProcessing()) + +
+ +

AI 분석 진행중...

+

분석이 완료되면 자동으로 결과가 표시됩니다.

+
+@endif + + +
+ + 인터뷰 원문 보기 + +
+
{{ $quotation->input_text }}
+
+
+@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/rd/index.blade.php b/resources/views/rd/index.blade.php new file mode 100644 index 00000000..b177fce4 --- /dev/null +++ b/resources/views/rd/index.blade.php @@ -0,0 +1,146 @@ +@extends('layouts.app') + +@section('title', '연구개발 대시보드') + +@section('content') + +
+

+ + 연구개발 대시보드 +

+ +
+ + +
+ +
+
+
+

전체 견적

+

{{ $dashboard['stats']['total'] }}

+
+
+ +
+
+
+ + +
+
+
+

분석 완료

+

{{ $dashboard['stats']['completed'] }}

+
+
+ +
+
+
+ + +
+
+
+

분석중

+

{{ $dashboard['stats']['processing'] }}

+
+
+ +
+
+
+ + +
+
+
+

실패

+

{{ $dashboard['stats']['failed'] }}

+
+
+ +
+
+
+
+ + +
+ + +
+
+ +
+
+

AI 견적 엔진

+

인터뷰 내용을 AI가 분석하여 SAM 표준 견적서를 자동 생성합니다.

+
+ Gemini + Claude + Phase 1 +
+
+
+
+ + +
+
+
+ +
+
+

모듈 카탈로그 관리

+

SAM 모듈 카탈로그를 관리하고 AI 프롬프트에 반영합니다.

+
+ Phase 3 예정 +
+
+
+
+
+ + +
+
+

최근 AI 견적 요청

+ 전체 보기 → +
+ +
+@endsection diff --git a/routes/api.php b/routes/api.php index 0a9f0721..f3bc0cb4 100644 --- a/routes/api.php +++ b/routes/api.php @@ -602,6 +602,20 @@ }); }); + /* + |-------------------------------------------------------------------------- + | 연구개발 API + |-------------------------------------------------------------------------- + */ + Route::prefix('rd')->name('rd.')->group(function () { + Route::prefix('ai-quotation')->name('ai-quotation.')->group(function () { + Route::get('/', [\App\Http\Controllers\Api\Admin\Rd\AiQuotationController::class, 'index'])->name('index'); + Route::post('/', [\App\Http\Controllers\Api\Admin\Rd\AiQuotationController::class, 'store'])->name('store'); + Route::get('/{id}', [\App\Http\Controllers\Api\Admin\Rd\AiQuotationController::class, 'show'])->name('show'); + Route::post('/{id}/analyze', [\App\Http\Controllers\Api\Admin\Rd\AiQuotationController::class, 'analyze'])->name('analyze'); + }); + }); + /* |-------------------------------------------------------------------------- | 일일 스크럼 API