feat: [ai-quotation] 제조 견적서 자동 생성 기능 추가
- AI 2단계 분석: 고객 인터뷰 → 요구사항 추출 → 견적 산출 - 모델 확장: AiQuotation(모드/견적번호), AiQuotationItem(규격/단가/금액) - AiQuotePriceTable 모델 신규 생성 - Create 페이지: 모듈/제조 모드 탭, 제품 카테고리, 고객 정보 입력 - Show 페이지: 제조 모드 분기 렌더링 (품목/금액/고객정보) - Edit 페이지: 품목 인라인 편집, 할인/부가세/조건 입력 - Document: 한국 표준 제조업 견적서 양식 템플릿 - Controller/Route: update 엔드포인트, edit 라우트 추가
This commit is contained in:
@@ -90,7 +90,12 @@ public function analyze(int $id): JsonResponse
|
||||
], 404);
|
||||
}
|
||||
|
||||
$result = $this->quotationService->runAnalysis($quotation);
|
||||
// 제조 모드는 제조용 분석 실행
|
||||
if ($quotation->isManufacture()) {
|
||||
$result = $this->quotationService->runManufactureAnalysis($quotation);
|
||||
} else {
|
||||
$result = $this->quotationService->runAnalysis($quotation);
|
||||
}
|
||||
|
||||
if ($result['ok']) {
|
||||
return response()->json([
|
||||
@@ -106,4 +111,26 @@ public function analyze(int $id): JsonResponse
|
||||
'error' => $result['error'],
|
||||
], 422);
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적 편집 저장
|
||||
*/
|
||||
public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$result = $this->quotationService->updateQuotation($id, $request->all());
|
||||
|
||||
if ($result['ok']) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '견적이 저장되었습니다.',
|
||||
'data' => $result['quotation'],
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '견적 저장에 실패했습니다.',
|
||||
'error' => $result['error'] ?? null,
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,4 +91,26 @@ public function showQuotation(Request $request, int $id): View|\Illuminate\Http\
|
||||
|
||||
return view('rd.ai-quotation.show', compact('quotation'));
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 견적 편집 (제조 모드)
|
||||
*/
|
||||
public function editQuotation(Request $request, int $id): View|\Illuminate\Http\Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('rd.ai-quotation.edit', $id));
|
||||
}
|
||||
|
||||
$quotation = $this->quotationService->getById($id);
|
||||
|
||||
if (! $quotation) {
|
||||
abort(404, 'AI 견적을 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
if (! $quotation->isCompleted()) {
|
||||
abort(403, '완료된 견적만 편집할 수 있습니다.');
|
||||
}
|
||||
|
||||
return view('rd.ai-quotation.edit', compact('quotation'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,12 @@ public function rules(): array
|
||||
'input_type' => 'required|in:text,voice,document',
|
||||
'input_text' => 'required_if:input_type,text|nullable|string',
|
||||
'ai_provider' => 'nullable|in:gemini,claude',
|
||||
'quote_mode' => 'nullable|in:module,manufacture',
|
||||
'product_category' => 'nullable|in:SCREEN,STEEL',
|
||||
'client_company' => 'nullable|string|max:200',
|
||||
'client_contact' => 'nullable|string|max:100',
|
||||
'client_phone' => 'nullable|string|max:50',
|
||||
'client_email' => 'nullable|email|max:200',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,9 @@ class AiQuotation extends Model
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'quote_mode',
|
||||
'quote_number',
|
||||
'product_category',
|
||||
'title',
|
||||
'input_type',
|
||||
'input_text',
|
||||
@@ -49,6 +52,10 @@ class AiQuotation extends Model
|
||||
|
||||
public const STATUS_FAILED = 'failed';
|
||||
|
||||
public const MODE_MODULE = 'module';
|
||||
|
||||
public const MODE_MANUFACTURE = 'manufacture';
|
||||
|
||||
public static function getStatuses(): array
|
||||
{
|
||||
return [
|
||||
@@ -118,6 +125,16 @@ public function setOption(string $key, $value): void
|
||||
$this->save();
|
||||
}
|
||||
|
||||
public function isManufacture(): bool
|
||||
{
|
||||
return $this->quote_mode === self::MODE_MANUFACTURE;
|
||||
}
|
||||
|
||||
public function isModule(): bool
|
||||
{
|
||||
return $this->quote_mode === self::MODE_MODULE;
|
||||
}
|
||||
|
||||
public function getFormattedDevCostAttribute(): string
|
||||
{
|
||||
return number_format((int) $this->total_dev_cost).'원';
|
||||
|
||||
@@ -14,6 +14,13 @@ class AiQuotationItem extends Model
|
||||
'module_id',
|
||||
'module_code',
|
||||
'module_name',
|
||||
'specification',
|
||||
'unit',
|
||||
'quantity',
|
||||
'unit_price',
|
||||
'total_price',
|
||||
'item_category',
|
||||
'floor_code',
|
||||
'is_required',
|
||||
'reason',
|
||||
'dev_cost',
|
||||
@@ -27,6 +34,9 @@ class AiQuotationItem extends Model
|
||||
'options' => 'array',
|
||||
'dev_cost' => 'decimal:0',
|
||||
'monthly_fee' => 'decimal:0',
|
||||
'quantity' => 'decimal:2',
|
||||
'unit_price' => 'decimal:2',
|
||||
'total_price' => 'decimal:2',
|
||||
];
|
||||
|
||||
public function quotation(): BelongsTo
|
||||
|
||||
78
app/Models/Rd/AiQuotePriceTable.php
Normal file
78
app/Models/Rd/AiQuotePriceTable.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Rd;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AiQuotePriceTable extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $table = 'ai_quote_price_tables';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'product_category',
|
||||
'price_type',
|
||||
'min_value',
|
||||
'max_value',
|
||||
'unit_price',
|
||||
'labor_rate',
|
||||
'install_rate',
|
||||
'is_active',
|
||||
'options',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'options' => 'array',
|
||||
'min_value' => 'decimal:4',
|
||||
'max_value' => 'decimal:4',
|
||||
'unit_price' => 'decimal:2',
|
||||
'labor_rate' => 'decimal:2',
|
||||
'install_rate' => 'decimal:2',
|
||||
];
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
public function scopeByCategory($query, string $category)
|
||||
{
|
||||
return $query->where('product_category', $category);
|
||||
}
|
||||
|
||||
public static function getPriceTablesForPrompt(?string $productCategory = null): array
|
||||
{
|
||||
$query = static::active()->orderBy('product_category')->orderBy('min_value');
|
||||
|
||||
if ($productCategory) {
|
||||
$query->byCategory($productCategory);
|
||||
}
|
||||
|
||||
return $query->get()->map(fn ($row) => [
|
||||
'product_category' => $row->product_category,
|
||||
'price_type' => $row->price_type,
|
||||
'min_value' => (float) $row->min_value,
|
||||
'max_value' => (float) $row->max_value,
|
||||
'unit_price' => (float) $row->unit_price,
|
||||
'labor_rate' => (float) $row->labor_rate,
|
||||
'install_rate' => (float) $row->install_rate,
|
||||
])->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();
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,11 @@
|
||||
use App\Models\Rd\AiQuotation;
|
||||
use App\Models\Rd\AiQuotationItem;
|
||||
use App\Models\Rd\AiQuotationModule;
|
||||
use App\Models\Rd\AiQuotePriceTable;
|
||||
use App\Models\System\AiConfig;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
@@ -52,9 +54,12 @@ public function getById(int $id): ?AiQuotation
|
||||
public function createAndAnalyze(array $data): array
|
||||
{
|
||||
$provider = $data['ai_provider'] ?? 'gemini';
|
||||
$quoteMode = $data['quote_mode'] ?? AiQuotation::MODE_MODULE;
|
||||
|
||||
$quotation = AiQuotation::create([
|
||||
'tenant_id' => session('selected_tenant_id', 1),
|
||||
'quote_mode' => $quoteMode,
|
||||
'product_category' => $data['product_category'] ?? null,
|
||||
'title' => $data['title'],
|
||||
'input_type' => $data['input_type'] ?? 'text',
|
||||
'input_text' => $data['input_text'] ?? null,
|
||||
@@ -63,6 +68,23 @@ public function createAndAnalyze(array $data): array
|
||||
'created_by' => Auth::id(),
|
||||
]);
|
||||
|
||||
// 제조 모드: 고객 정보를 options에 저장
|
||||
if ($quoteMode === AiQuotation::MODE_MANUFACTURE) {
|
||||
$quotation->update([
|
||||
'quote_number' => $this->generateQuoteNumber($data['product_category'] ?? 'SC'),
|
||||
'options' => array_filter([
|
||||
'client' => array_filter([
|
||||
'company' => $data['client_company'] ?? null,
|
||||
'contact' => $data['client_contact'] ?? null,
|
||||
'phone' => $data['client_phone'] ?? null,
|
||||
'email' => $data['client_email'] ?? null,
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
|
||||
return $this->runManufactureAnalysis($quotation);
|
||||
}
|
||||
|
||||
return $this->runAnalysis($quotation);
|
||||
}
|
||||
|
||||
@@ -400,6 +422,468 @@ private function saveQuotationItems(AiQuotation $quotation, array $quotationResu
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================
|
||||
// 제조 견적 (Manufacture) 전용 메서드
|
||||
// ===================================================
|
||||
|
||||
/**
|
||||
* 제조 견적 AI 분석 실행
|
||||
*/
|
||||
public function runManufactureAnalysis(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 설정이 없습니다.");
|
||||
}
|
||||
|
||||
$productCategory = $quotation->product_category ?? 'SCREEN';
|
||||
$priceTables = AiQuotePriceTable::getPriceTablesForPrompt($productCategory);
|
||||
|
||||
// 1차: 요구사항 분석
|
||||
$analysisPrompt = $this->buildManufactureAnalysisPrompt(
|
||||
$quotation->input_text,
|
||||
$productCategory
|
||||
);
|
||||
$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->buildManufactureQuotationPrompt(
|
||||
$analysisResult,
|
||||
$priceTables,
|
||||
$productCategory
|
||||
);
|
||||
$quotationRaw = $this->callAi($config, $provider, $quotationPrompt, 'AI제조견적-산출');
|
||||
|
||||
$quotationResult = $this->parseJsonResponse($quotationRaw);
|
||||
if (! $quotationResult) {
|
||||
throw new \RuntimeException('AI 견적 산출 결과 파싱 실패');
|
||||
}
|
||||
|
||||
// 품목 저장
|
||||
$this->saveManufactureItems($quotation, $quotationResult);
|
||||
|
||||
// 합계 계산 + options에 pricing 저장
|
||||
$totals = $quotation->items()->selectRaw(
|
||||
'SUM(total_price) as subtotal'
|
||||
)->first();
|
||||
|
||||
$subtotal = (int) ($totals->subtotal ?? 0);
|
||||
$pricing = $quotationResult['pricing'] ?? [];
|
||||
$discountRate = (float) ($pricing['discount_rate'] ?? 0);
|
||||
$discountAmount = (int) round($subtotal * $discountRate / 100);
|
||||
$afterDiscount = $subtotal - $discountAmount;
|
||||
$vatAmount = (int) round($afterDiscount * 0.1);
|
||||
$finalAmount = $afterDiscount + $vatAmount;
|
||||
|
||||
// 고객 정보 업데이트 (AI가 추출한 정보로 보강)
|
||||
$clientFromAi = $analysisResult['client'] ?? [];
|
||||
$existingOptions = $quotation->options ?? [];
|
||||
$existingClient = $existingOptions['client'] ?? [];
|
||||
$mergedClient = array_filter(array_merge($clientFromAi, $existingClient));
|
||||
|
||||
$existingOptions['client'] = $mergedClient;
|
||||
$existingOptions['project'] = $analysisResult['project'] ?? [];
|
||||
$existingOptions['pricing'] = [
|
||||
'subtotal' => $subtotal,
|
||||
'material_cost' => (int) ($pricing['material_cost'] ?? 0),
|
||||
'labor_cost' => (int) ($pricing['labor_cost'] ?? 0),
|
||||
'install_cost' => (int) ($pricing['install_cost'] ?? 0),
|
||||
'discount_rate' => $discountRate,
|
||||
'discount_amount' => $discountAmount,
|
||||
'vat_amount' => $vatAmount,
|
||||
'final_amount' => $finalAmount,
|
||||
];
|
||||
$existingOptions['terms'] = $quotationResult['terms'] ?? [
|
||||
'valid_until' => now()->addDays(30)->format('Y-m-d'),
|
||||
'payment' => '계약 시 50%, 설치 완료 후 50%',
|
||||
'delivery' => '계약 후 4주 이내',
|
||||
];
|
||||
|
||||
$quotation->update([
|
||||
'quotation_result' => $quotationResult,
|
||||
'ai_model' => $config->model,
|
||||
'total_dev_cost' => $finalAmount,
|
||||
'total_monthly_fee' => 0,
|
||||
'options' => $existingOptions,
|
||||
'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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적번호 자동 생성
|
||||
*/
|
||||
public function generateQuoteNumber(?string $productCategory): string
|
||||
{
|
||||
$prefix = match (strtoupper($productCategory ?? 'SC')) {
|
||||
'SCREEN' => 'SC',
|
||||
'STEEL' => 'ST',
|
||||
default => 'SC',
|
||||
};
|
||||
|
||||
$dateStr = now()->format('ymd');
|
||||
$baseNumber = "AQ-{$prefix}-{$dateStr}";
|
||||
|
||||
$count = AiQuotation::where('quote_number', 'like', "{$baseNumber}-%")->count();
|
||||
$seq = str_pad($count + 1, 2, '0', STR_PAD_LEFT);
|
||||
|
||||
return "{$baseNumber}-{$seq}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 제조 견적 품목 저장
|
||||
*/
|
||||
private function saveManufactureItems(AiQuotation $quotation, array $quotationResult): void
|
||||
{
|
||||
$quotation->items()->delete();
|
||||
|
||||
$items = $quotationResult['items'] ?? [];
|
||||
|
||||
foreach ($items as $index => $item) {
|
||||
$quantity = (float) ($item['quantity'] ?? 1);
|
||||
$unitPrice = (float) ($item['unit_price'] ?? 0);
|
||||
$totalPrice = (float) ($item['total_price'] ?? ($quantity * $unitPrice));
|
||||
|
||||
AiQuotationItem::create([
|
||||
'ai_quotation_id' => $quotation->id,
|
||||
'module_code' => $item['item_code'] ?? '',
|
||||
'module_name' => $item['item_name'] ?? '',
|
||||
'specification' => $item['specification'] ?? null,
|
||||
'unit' => $item['unit'] ?? 'SET',
|
||||
'quantity' => $quantity,
|
||||
'unit_price' => $unitPrice,
|
||||
'total_price' => $totalPrice,
|
||||
'item_category' => $item['item_category'] ?? 'material',
|
||||
'floor_code' => $item['floor_code'] ?? null,
|
||||
'reason' => $item['description'] ?? null,
|
||||
'sort_order' => $index,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적 편집 저장
|
||||
*/
|
||||
public function updateQuotation(int $id, array $data): array
|
||||
{
|
||||
$quotation = $this->getById($id);
|
||||
|
||||
if (! $quotation) {
|
||||
return ['ok' => false, 'error' => '견적을 찾을 수 없습니다.'];
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
// options 업데이트
|
||||
$options = $quotation->options ?? [];
|
||||
|
||||
if (isset($data['client'])) {
|
||||
$options['client'] = $data['client'];
|
||||
}
|
||||
if (isset($data['project'])) {
|
||||
$options['project'] = $data['project'];
|
||||
}
|
||||
if (isset($data['terms'])) {
|
||||
$options['terms'] = $data['terms'];
|
||||
}
|
||||
|
||||
// 품목 업데이트
|
||||
if (isset($data['items'])) {
|
||||
$quotation->items()->delete();
|
||||
$subtotal = 0;
|
||||
$materialCost = 0;
|
||||
$laborCost = 0;
|
||||
$installCost = 0;
|
||||
|
||||
foreach ($data['items'] as $index => $item) {
|
||||
$qty = (float) ($item['quantity'] ?? 1);
|
||||
$price = (float) ($item['unit_price'] ?? 0);
|
||||
$total = round($qty * $price, 2);
|
||||
$subtotal += $total;
|
||||
|
||||
$cat = $item['item_category'] ?? 'material';
|
||||
if ($cat === 'material') {
|
||||
$materialCost += $total;
|
||||
} elseif ($cat === 'labor') {
|
||||
$laborCost += $total;
|
||||
} elseif ($cat === 'install') {
|
||||
$installCost += $total;
|
||||
}
|
||||
|
||||
AiQuotationItem::create([
|
||||
'ai_quotation_id' => $quotation->id,
|
||||
'module_code' => $item['item_code'] ?? '',
|
||||
'module_name' => $item['item_name'] ?? '',
|
||||
'specification' => $item['specification'] ?? null,
|
||||
'unit' => $item['unit'] ?? 'SET',
|
||||
'quantity' => $qty,
|
||||
'unit_price' => $price,
|
||||
'total_price' => $total,
|
||||
'item_category' => $cat,
|
||||
'floor_code' => $item['floor_code'] ?? null,
|
||||
'reason' => $item['description'] ?? null,
|
||||
'sort_order' => $index,
|
||||
]);
|
||||
}
|
||||
|
||||
// 가격 재계산
|
||||
$discountRate = (float) ($data['discount_rate'] ?? $options['pricing']['discount_rate'] ?? 0);
|
||||
$discountAmount = (int) round($subtotal * $discountRate / 100);
|
||||
$afterDiscount = $subtotal - $discountAmount;
|
||||
$vatAmount = (int) round($afterDiscount * 0.1);
|
||||
$finalAmount = $afterDiscount + $vatAmount;
|
||||
|
||||
$options['pricing'] = [
|
||||
'subtotal' => (int) $subtotal,
|
||||
'material_cost' => (int) $materialCost,
|
||||
'labor_cost' => (int) $laborCost,
|
||||
'install_cost' => (int) $installCost,
|
||||
'discount_rate' => $discountRate,
|
||||
'discount_amount' => $discountAmount,
|
||||
'vat_amount' => $vatAmount,
|
||||
'final_amount' => $finalAmount,
|
||||
];
|
||||
|
||||
$quotation->update([
|
||||
'total_dev_cost' => $finalAmount,
|
||||
'total_monthly_fee' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
$quotation->update(['options' => $options]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'quotation' => $quotation->fresh(['items', 'creator:id,name']),
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
Log::error('견적 편집 저장 실패', [
|
||||
'quotation_id' => $id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return ['ok' => false, 'error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 금액을 한글로 변환
|
||||
*/
|
||||
public static function numberToKorean(int $number): string
|
||||
{
|
||||
if ($number === 0) {
|
||||
return '영';
|
||||
}
|
||||
|
||||
$units = ['', '만', '억', '조'];
|
||||
$digits = ['', '일', '이', '삼', '사', '오', '육', '칠', '팔', '구'];
|
||||
$subUnits = ['', '십', '백', '천'];
|
||||
|
||||
$result = '';
|
||||
$unitIndex = 0;
|
||||
|
||||
while ($number > 0) {
|
||||
$chunk = $number % 10000;
|
||||
if ($chunk > 0) {
|
||||
$chunkStr = '';
|
||||
$subIndex = 0;
|
||||
$temp = $chunk;
|
||||
while ($temp > 0) {
|
||||
$digit = $temp % 10;
|
||||
if ($digit > 0) {
|
||||
$prefix = ($digit === 1 && $subIndex > 0) ? '' : $digits[$digit];
|
||||
$chunkStr = $prefix.$subUnits[$subIndex].$chunkStr;
|
||||
}
|
||||
$temp = (int) ($temp / 10);
|
||||
$subIndex++;
|
||||
}
|
||||
$result = $chunkStr.$units[$unitIndex].$result;
|
||||
}
|
||||
$number = (int) ($number / 10000);
|
||||
$unitIndex++;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
// ===================================================
|
||||
// 제조 견적 AI 프롬프트
|
||||
// ===================================================
|
||||
|
||||
/**
|
||||
* 제조 견적용 1단계: 요구사항 분석
|
||||
*/
|
||||
private function buildManufactureAnalysisPrompt(string $interviewText, string $productCategory): string
|
||||
{
|
||||
$categoryLabel = $productCategory === 'STEEL' ? '철재(방화문/방화셔터)' : '방화스크린';
|
||||
|
||||
return <<<PROMPT
|
||||
당신은 {$categoryLabel} 제조업체의 영업 전문가입니다.
|
||||
고객 인터뷰/상담 내용을 분석하여 견적에 필요한 정보를 구조화된 JSON으로 추출하세요.
|
||||
|
||||
## 상담 내용
|
||||
{$interviewText}
|
||||
|
||||
## 추출 대상
|
||||
1. 고객 정보 (회사명, 담당자, 연락처, 이메일, 주소)
|
||||
2. 프로젝트 정보 (현장명, 위치, 건물유형, 용도)
|
||||
3. 위치별 제품 사양 (층/부호, 제품유형, 개구부 크기 W×H mm, 수량, 가이드레일 유형, 모터, 특이사항)
|
||||
4. 설치 조건 (접근성, 전원, 층고 등)
|
||||
5. 고객 특이 요청사항
|
||||
|
||||
## 출력 형식 (반드시 이 JSON 구조를 따르세요)
|
||||
{
|
||||
"client": {
|
||||
"company": "회사명 또는 null",
|
||||
"contact": "담당자명 또는 null",
|
||||
"phone": "연락처 또는 null",
|
||||
"email": "이메일 또는 null",
|
||||
"address": "주소 또는 null"
|
||||
},
|
||||
"project": {
|
||||
"name": "현장명/프로젝트명",
|
||||
"location": "현장 위치",
|
||||
"building_type": "건물유형 (오피스/상가/공장 등)",
|
||||
"purpose": "용도"
|
||||
},
|
||||
"product_specs": [
|
||||
{
|
||||
"floor_code": "위치 코드 (예: B1-A01)",
|
||||
"floor_name": "위치 설명 (예: 지하1층 A구역)",
|
||||
"product_type": "SCREEN 또는 STEEL",
|
||||
"width_mm": 3000,
|
||||
"height_mm": 2500,
|
||||
"quantity": 1,
|
||||
"guide_rail": "벽부형/노출형/매립형",
|
||||
"motor": "단상/삼상",
|
||||
"note": "특이사항 또는 null"
|
||||
}
|
||||
],
|
||||
"install_conditions": {
|
||||
"accessibility": "양호/보통/불량",
|
||||
"power_source": "단상/삼상",
|
||||
"ceiling_height": "층고 정보 또는 null",
|
||||
"special_requirements": "특수 요구사항 또는 null"
|
||||
},
|
||||
"customer_requests": "고객 특이 요청사항 텍스트 또는 null"
|
||||
}
|
||||
|
||||
중요: JSON만 출력하세요. 설명이나 마크다운 코드 블록 없이 순수 JSON만 반환하세요.
|
||||
인터뷰에서 명시되지 않은 정보는 null로 설정하세요.
|
||||
PROMPT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 제조 견적용 2단계: 견적 산출
|
||||
*/
|
||||
private function buildManufactureQuotationPrompt(array $analysisResult, array $priceTables, string $productCategory): string
|
||||
{
|
||||
$analysisJson = json_encode($analysisResult, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||
$priceJson = json_encode($priceTables, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||
$categoryLabel = $productCategory === 'STEEL' ? '철재' : '방화스크린';
|
||||
|
||||
return <<<PROMPT
|
||||
아래 분석 결과를 바탕으로 {$categoryLabel} 제조 견적을 산출하세요.
|
||||
|
||||
## 분석 결과
|
||||
{$analysisJson}
|
||||
|
||||
## 단가표
|
||||
{$priceJson}
|
||||
|
||||
## 산출 규칙 (방화스크린 기준)
|
||||
1. 유효 면적 계산: M = (W + 140) × (H + 350) / 1,000,000 (단위: ㎡)
|
||||
- W: 개구부 폭(mm), H: 개구부 높이(mm)
|
||||
- 140, 350은 가이드레일/하부 여유분
|
||||
2. 단가 적용: 면적 구간별 단가표 참조 (단가표가 없으면 ㎡당 350,000원 기본 적용)
|
||||
3. 재료비 = 면적 × 단가 × 수량
|
||||
4. 노무비 = 재료비 × 노무비율 (단가표 labor_rate, 기본 15%)
|
||||
5. 설치비 = 재료비 × 설치비율 (단가표 install_rate, 기본 10%)
|
||||
6. 가이드레일, 케이스, 모터 등 부대재료는 별도 행으로 산출
|
||||
|
||||
## 출력 형식 (반드시 이 JSON 구조를 따르세요)
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"item_code": "SCR-001",
|
||||
"item_name": "방화스크린 본체",
|
||||
"specification": "3140×2850",
|
||||
"unit": "SET",
|
||||
"quantity": 1,
|
||||
"unit_price": 350000,
|
||||
"total_price": 350000,
|
||||
"item_category": "material",
|
||||
"floor_code": "B1-A01",
|
||||
"description": "지하1층 A구역 방화스크린"
|
||||
},
|
||||
{
|
||||
"item_code": "LBR-001",
|
||||
"item_name": "설치 노무비",
|
||||
"specification": "",
|
||||
"unit": "식",
|
||||
"quantity": 1,
|
||||
"unit_price": 52500,
|
||||
"total_price": 52500,
|
||||
"item_category": "labor",
|
||||
"floor_code": "",
|
||||
"description": "스크린 설치 인건비"
|
||||
}
|
||||
],
|
||||
"pricing": {
|
||||
"material_cost": 0,
|
||||
"labor_cost": 0,
|
||||
"install_cost": 0,
|
||||
"discount_rate": 0,
|
||||
"note": "할인 근거 또는 null"
|
||||
},
|
||||
"terms": {
|
||||
"valid_until": "유효기간 날짜 (YYYY-MM-DD)",
|
||||
"payment": "결제 조건",
|
||||
"delivery": "납기 조건"
|
||||
}
|
||||
}
|
||||
|
||||
중요:
|
||||
- JSON만 출력하세요. 설명이나 마크다운 코드 블록 없이 순수 JSON만 반환하세요.
|
||||
- item_category는 반드시 "material", "labor", "install" 중 하나여야 합니다.
|
||||
- 모든 금액은 원 단위 정수로 작성하세요.
|
||||
- 위치별로 재료비 품목을 각각 작성하고, 노무비와 설치비는 전체 합산 1행으로 작성하세요.
|
||||
PROMPT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 대시보드 통계
|
||||
*/
|
||||
|
||||
@@ -18,10 +18,68 @@
|
||||
<div class="bg-white rounded-lg shadow-sm">
|
||||
<div class="px-6 py-4 border-b border-gray-100">
|
||||
<h2 class="text-lg font-semibold text-gray-800">인터뷰 내용 입력</h2>
|
||||
<p class="text-sm text-gray-500 mt-1">고객사 인터뷰 내용을 입력하면 AI가 업무를 분석하고 맞춤형 견적서를 자동 생성합니다.</p>
|
||||
<p class="text-sm text-gray-500 mt-1">고객사 인터뷰 내용을 입력하면 AI가 분석하고 맞춤형 견적서를 자동 생성합니다.</p>
|
||||
</div>
|
||||
|
||||
<form id="quotationForm" class="p-6 space-y-6">
|
||||
<!-- 견적 모드 선택 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">견적 모드</label>
|
||||
<div class="flex gap-3">
|
||||
<label id="modeModule" class="flex items-center gap-2 px-4 py-2.5 border border-purple-500 bg-purple-50 text-purple-700 rounded-lg cursor-pointer transition">
|
||||
<input type="radio" name="quote_mode" value="module" checked class="text-purple-600" onchange="updateModeUI()">
|
||||
<i class="ri-apps-line"></i> 모듈 추천
|
||||
</label>
|
||||
<label id="modeManufacture" class="flex items-center gap-2 px-4 py-2.5 border border-gray-300 text-gray-600 rounded-lg cursor-pointer transition">
|
||||
<input type="radio" name="quote_mode" value="manufacture" class="text-blue-600" onchange="updateModeUI()">
|
||||
<i class="ri-building-4-line"></i> 제조 견적
|
||||
<span class="text-xs bg-blue-100 text-blue-600 px-1.5 py-0.5 rounded">NEW</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 제조 견적 전용: 제품 카테고리 -->
|
||||
<div id="manufactureSections" class="hidden space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">제품 카테고리 <span class="text-red-500">*</span></label>
|
||||
<div class="flex gap-3">
|
||||
<label class="flex items-center gap-2 px-4 py-2.5 border rounded-lg cursor-pointer transition hover:bg-blue-50" id="catScreen">
|
||||
<input type="radio" name="product_category" value="SCREEN" checked class="text-blue-600" onchange="updateCategoryUI()">
|
||||
<span class="font-medium">방화스크린</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 px-4 py-2.5 border rounded-lg cursor-pointer transition hover:bg-orange-50" id="catSteel">
|
||||
<input type="radio" name="product_category" value="STEEL" class="text-orange-600" onchange="updateCategoryUI()">
|
||||
<span class="font-medium">철재</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 고객 정보 (선택) -->
|
||||
<div class="bg-blue-50 rounded-lg p-4">
|
||||
<h3 class="text-sm font-semibold text-blue-700 mb-3 flex items-center gap-1">
|
||||
<i class="ri-user-line"></i> 고객 정보 (선택 — AI가 인터뷰에서 자동 추출합니다)
|
||||
</h3>
|
||||
<div class="grid gap-3" style="grid-template-columns: repeat(2, 1fr);">
|
||||
<div>
|
||||
<label class="block text-xs text-gray-600 mb-1">회사명</label>
|
||||
<input type="text" name="client_company" placeholder="(주)한빛건설" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-600 mb-1">담당자</label>
|
||||
<input type="text" name="client_contact" placeholder="김철수 과장" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-600 mb-1">연락처</label>
|
||||
<input type="text" name="client_phone" placeholder="010-1234-5678" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-600 mb-1">이메일</label>
|
||||
<input type="email" name="client_email" placeholder="cs.kim@hanbit.co.kr" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 제목 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">견적 제목 <span class="text-red-500">*</span></label>
|
||||
@@ -101,6 +159,45 @@ class="px-6 py-2.5 bg-purple-600 text-white rounded-lg hover:bg-purple-700 trans
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function getQuoteMode() {
|
||||
return document.querySelector('input[name="quote_mode"]:checked')?.value || 'module';
|
||||
}
|
||||
|
||||
function updateModeUI() {
|
||||
const mode = getQuoteMode();
|
||||
const modeModule = document.getElementById('modeModule');
|
||||
const modeManufacture = document.getElementById('modeManufacture');
|
||||
const mfgSections = document.getElementById('manufactureSections');
|
||||
const titleInput = document.getElementById('inputTitle');
|
||||
|
||||
if (mode === 'manufacture') {
|
||||
modeModule.className = 'flex items-center gap-2 px-4 py-2.5 border border-gray-300 text-gray-600 rounded-lg cursor-pointer transition';
|
||||
modeManufacture.className = 'flex items-center gap-2 px-4 py-2.5 border border-blue-500 bg-blue-50 text-blue-700 rounded-lg cursor-pointer transition';
|
||||
mfgSections.classList.remove('hidden');
|
||||
titleInput.placeholder = '예: (주)한빛건설 오피스빌딩 방화스크린';
|
||||
} else {
|
||||
modeModule.className = 'flex items-center gap-2 px-4 py-2.5 border border-purple-500 bg-purple-50 text-purple-700 rounded-lg cursor-pointer transition';
|
||||
modeManufacture.className = 'flex items-center gap-2 px-4 py-2.5 border border-gray-300 text-gray-600 rounded-lg cursor-pointer transition';
|
||||
mfgSections.classList.add('hidden');
|
||||
titleInput.placeholder = '예: (주)대한기계 ERP 도입 견적';
|
||||
}
|
||||
|
||||
updateCategoryUI();
|
||||
}
|
||||
|
||||
function updateCategoryUI() {
|
||||
const cat = document.querySelector('input[name="product_category"]:checked')?.value || 'SCREEN';
|
||||
const catScreen = document.getElementById('catScreen');
|
||||
const catSteel = document.getElementById('catSteel');
|
||||
|
||||
catScreen.classList.toggle('border-blue-500', cat === 'SCREEN');
|
||||
catScreen.classList.toggle('bg-blue-50', cat === 'SCREEN');
|
||||
catScreen.classList.toggle('border-gray-300', cat !== 'SCREEN');
|
||||
catSteel.classList.toggle('border-orange-500', cat === 'STEEL');
|
||||
catSteel.classList.toggle('bg-orange-50', cat === 'STEEL');
|
||||
catSteel.classList.toggle('border-gray-300', cat !== 'STEEL');
|
||||
}
|
||||
|
||||
function updateProviderUI() {
|
||||
const gemini = document.getElementById('providerGemini');
|
||||
const claude = document.getElementById('providerClaude');
|
||||
@@ -155,10 +252,8 @@ function updateProviderUI() {
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.data?.id) {
|
||||
// 성공 — 상세 페이지로 이동
|
||||
window.location.href = `{{ url('/rd/ai-quotation') }}/${result.data.id}`;
|
||||
} else {
|
||||
// 실패
|
||||
alert(result.message || 'AI 분석에 실패했습니다.');
|
||||
if (result.data?.id) {
|
||||
window.location.href = `{{ url('/rd/ai-quotation') }}/${result.data.id}`;
|
||||
@@ -175,8 +270,44 @@ function updateProviderUI() {
|
||||
});
|
||||
|
||||
function fillSampleInterview() {
|
||||
document.getElementById('inputTitle').value = '(주)대한기계 ERP 도입 견적';
|
||||
document.getElementById('inputText').value = `Q: 현재 회사 규모와 업종을 알려주세요.
|
||||
const mode = getQuoteMode();
|
||||
|
||||
if (mode === 'manufacture') {
|
||||
document.getElementById('inputTitle').value = '(주)한빛건설 오피스빌딩 방화스크린';
|
||||
document.getElementById('inputText').value = `Q: 어떤 건물인가요?
|
||||
A: 지하1층 지상5층 오피스빌딩입니다. 신축 건물이에요. 인천 남동구에 있습니다.
|
||||
|
||||
Q: 어떤 제품이 필요하세요?
|
||||
A: 지하1층 주차장 진입부에 방화스크린 2개소, 1층 로비에 3개소 필요합니다.
|
||||
|
||||
Q: 규격을 알려주세요.
|
||||
A: 지하1층은 A구역 3000×2500, B구역 4000×3000이고,
|
||||
1층은 C구역 2500×2000 2개소, D구역 5000×3500 1개소입니다.
|
||||
|
||||
Q: 가이드레일과 모터는?
|
||||
A: 전부 벽부형이고 단상 전원입니다. 가이드레일은 일반형으로 해주세요.
|
||||
|
||||
Q: 설치 조건은?
|
||||
A: 접근성 양호하고요. 층고는 지하 3m, 1층 2.8m입니다.
|
||||
|
||||
Q: 특별히 요청사항이 있으신가요?
|
||||
A: 1층 D구역은 개구부가 넓어서 분할 설치가 필요한지 확인 부탁드립니다.
|
||||
|
||||
Q: 회사 정보를 알려주세요.
|
||||
A: (주)한빛건설, 김철수 과장, 010-1234-5678, cs.kim@hanbit.co.kr`;
|
||||
|
||||
// 고객 정보 자동 입력
|
||||
const clientCompany = document.querySelector('input[name="client_company"]');
|
||||
const clientContact = document.querySelector('input[name="client_contact"]');
|
||||
const clientPhone = document.querySelector('input[name="client_phone"]');
|
||||
const clientEmail = document.querySelector('input[name="client_email"]');
|
||||
if (clientCompany) clientCompany.value = '(주)한빛건설';
|
||||
if (clientContact) clientContact.value = '김철수 과장';
|
||||
if (clientPhone) clientPhone.value = '010-1234-5678';
|
||||
if (clientEmail) clientEmail.value = 'cs.kim@hanbit.co.kr';
|
||||
} else {
|
||||
document.getElementById('inputTitle').value = '(주)대한기계 ERP 도입 견적';
|
||||
document.getElementById('inputText').value = `Q: 현재 회사 규모와 업종을 알려주세요.
|
||||
A: 저희는 기계부품 제조업체입니다. 직원이 45명 정도 되고, 공장 1곳에서 CNC 가공이랑 프레스 작업을 주로 하고 있어요. 사무직이 12명, 생산직이 33명입니다.
|
||||
|
||||
Q: 현재 업무 관리는 어떻게 하고 계세요?
|
||||
@@ -188,20 +319,13 @@ function fillSampleInterview() {
|
||||
Q: 생산 현장은요?
|
||||
A: 작업일보를 종이에 써서 제출해요. 작업지시서도 종이로 출력해서 현장에 붙여놓고요. 불량이 나면 어디서 발생했는지 추적이 잘 안 돼요. LOT 관리를 하고 싶은데 현재는 수기로 하다 보니 한계가 있어요. 납기 관리도 엑셀인데, 긴급 주문 들어오면 일정 조정하느라 난리예요.
|
||||
|
||||
Q: 자재/재고 관리는요?
|
||||
A: 재고는 한 달에 한 번 실사를 해봐야 알 수 있어요. 원자재가 얼마나 남았는지 실시간으로 파악이 안 되니까, 급하면 긴급 발주를 넣고 비싼 값에 사오는 경우도 많아요. 발주서도 엑셀로 만들어서 팩스로 보내요.
|
||||
|
||||
Q: 품질 관리 쪽은 어떤 상황인가요?
|
||||
A: 수입검사는 하고 있긴 한데 검사 기록을 종이 대장에 적어요. 고객사에서 검사성적서 요청하면 수작업으로 만들어야 해서 시간이 오래 걸려요. ISO 인증 심사 때마다 서류 준비하느라 야근해요.
|
||||
|
||||
Q: 회계/재무 쪽은요?
|
||||
A: 세금계산서는 홈택스에서 발행하고 있어요. 미수금 관리가 안 돼서 어떤 거래처가 얼마 미지급인지 파악하려면 한참 뒤져야 해요. 매출 현황도 월말에 정리하는 식이라 실시간 파악이 불가능해요.
|
||||
|
||||
Q: 가장 시급하게 개선하고 싶은 부분은?
|
||||
A: 일단 생산 현황을 실시간으로 볼 수 있었으면 좋겠고, 재고 관리가 급해요. 그리고 직원 관리랑 급여 계산을 자동화하고 싶어요. 영업 쪽도 견적서 관리가 너무 안 되니까 개선이 필요해요.`;
|
||||
}
|
||||
}
|
||||
|
||||
// 초기 Provider UI
|
||||
// 초기 UI
|
||||
updateProviderUI();
|
||||
updateModeUI();
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
{{-- 제조 견적서 클래식 템플릿 — 한국 표준 제조업 견적서 양식 --}}
|
||||
<style>
|
||||
.mfg-classic .doc-table { width: 100%; border-collapse: collapse; }
|
||||
.mfg-classic .doc-table th, .mfg-classic .doc-table td { border: 1px solid #333; padding: 6px 10px; font-size: 13px; }
|
||||
.mfg-classic .doc-table th { background-color: #f3f4f6; font-weight: 600; }
|
||||
.mfg-classic .seal-box {
|
||||
display: inline-block; width: 60px; height: 60px; border: 2px solid #dc2626;
|
||||
border-radius: 50%; text-align: center; line-height: 56px; color: #dc2626;
|
||||
font-weight: 700; font-size: 16px;
|
||||
}
|
||||
@media print {
|
||||
.mfg-classic { padding: 10mm 15mm !important; box-shadow: none !important; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@php
|
||||
$materialItems = $quotation->items->where('item_category', 'material');
|
||||
$laborItems = $quotation->items->where('item_category', 'labor');
|
||||
$installItems = $quotation->items->where('item_category', 'install');
|
||||
$otherItems = $quotation->items->whereNotIn('item_category', ['material', 'labor', 'install']);
|
||||
$allItems = $materialItems->merge($laborItems)->merge($installItems)->merge($otherItems);
|
||||
|
||||
$materialTotal = $materialItems->sum('total_price');
|
||||
$laborTotal = $laborItems->sum('total_price');
|
||||
$installTotal = $installItems->sum('total_price');
|
||||
@endphp
|
||||
|
||||
<div class="mfg-classic document-page max-w-[210mm] mx-auto my-8 bg-white shadow-lg" style="padding: 15mm 20mm;">
|
||||
|
||||
{{-- 제목 --}}
|
||||
<h1 class="text-center text-3xl font-bold tracking-[0.5em] mb-8 pb-4 border-b-2 border-gray-800">
|
||||
견 적 서
|
||||
</h1>
|
||||
|
||||
{{-- 견적 정보 --}}
|
||||
<div class="flex justify-between mb-6 text-sm">
|
||||
<div>
|
||||
<p><span class="font-semibold">견적번호:</span> {{ $quotationNo }}</p>
|
||||
<p><span class="font-semibold">유효기간:</span> {{ $validUntil }}</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p><span class="font-semibold">견적일자:</span> {{ $quotation->created_at->format('Y년 m월 d일') }}</p>
|
||||
<p><span class="font-semibold">제품구분:</span> {{ $quotation->product_category === 'STEEL' ? '철재' : '방화스크린' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 수신 / 공급자 --}}
|
||||
<table class="doc-table mb-6">
|
||||
<colgroup>
|
||||
<col style="width: 8%;"><col style="width: 42%;"><col style="width: 8%;"><col style="width: 42%;">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2" class="text-center" style="background-color: #eff6ff;">수 신</th>
|
||||
<th colspan="2" class="text-center" style="background-color: #f0fdf4;">공 급 자</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>상 호</th><td>{{ $client['company'] ?? $quotation->title }}</td>
|
||||
<th>상 호</th><td>(주)코드브릿지엑스</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>담당자</th><td>{{ $client['contact'] ?? '-' }}</td>
|
||||
<th>대 표</th><td>이의찬</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>연락처</th><td>{{ $client['phone'] ?? '-' }}</td>
|
||||
<th>주 소</th><td>인천 남동구 남동대로 215번길 30</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>이메일</th><td>{{ $client['email'] ?? '-' }}</td>
|
||||
<th>연락처</th><td>032-123-4567</td>
|
||||
</tr>
|
||||
@if(!empty($project['name']))
|
||||
<tr>
|
||||
<th>현장명</th><td colspan="3">{{ $project['name'] }}{{ !empty($project['location']) ? ' ('.$project['location'].')' : '' }}</td>
|
||||
</tr>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{-- 합계 --}}
|
||||
<div class="mb-6 p-4 border-2 border-gray-800 text-center">
|
||||
<p class="text-sm mb-2">아래와 같이 견적합니다.</p>
|
||||
<p class="text-xl font-bold">
|
||||
합계금액: 금 {{ $subtotalKorean }}원정
|
||||
<span class="text-base font-normal">(₩{{ number_format($subtotal) }})</span>
|
||||
</p>
|
||||
<p class="text-xs text-gray-600 mt-1">※ 부가가치세 별도</p>
|
||||
</div>
|
||||
|
||||
{{-- 품목 테이블 --}}
|
||||
<table class="doc-table mb-6">
|
||||
<colgroup>
|
||||
<col style="width: 5%;"><col style="width: 10%;"><col style="width: 20%;">
|
||||
<col style="width: 15%;"><col style="width: 7%;"><col style="width: 7%;">
|
||||
<col style="width: 18%;"><col style="width: 18%;">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center">No</th>
|
||||
<th class="text-center">위치</th>
|
||||
<th class="text-center">품 목</th>
|
||||
<th class="text-center">규 격</th>
|
||||
<th class="text-center">단위</th>
|
||||
<th class="text-center">수량</th>
|
||||
<th class="text-center">단가 (원)</th>
|
||||
<th class="text-center">금액 (원)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@php $no = 1; @endphp
|
||||
|
||||
{{-- 재료비 --}}
|
||||
@if($materialItems->isNotEmpty())
|
||||
<tr style="background-color: #eff6ff;">
|
||||
<td colspan="8" class="text-sm font-semibold" style="padding: 4px 10px;">[ 재료비 ]</td>
|
||||
</tr>
|
||||
@foreach($materialItems as $item)
|
||||
<tr>
|
||||
<td class="text-center">{{ $no++ }}</td>
|
||||
<td class="text-center text-xs">{{ $item->floor_code }}</td>
|
||||
<td>{{ $item->module_name }}</td>
|
||||
<td class="text-center text-xs">{{ $item->specification }}</td>
|
||||
<td class="text-center">{{ $item->unit }}</td>
|
||||
<td class="text-center">{{ number_format((float)$item->quantity, 0) }}</td>
|
||||
<td class="text-right">{{ number_format((float)$item->unit_price) }}</td>
|
||||
<td class="text-right">{{ number_format((float)$item->total_price) }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@endif
|
||||
|
||||
{{-- 노무비 --}}
|
||||
@if($laborItems->isNotEmpty())
|
||||
<tr style="background-color: #f0fdf4;">
|
||||
<td colspan="8" class="text-sm font-semibold" style="padding: 4px 10px;">[ 노무비 ]</td>
|
||||
</tr>
|
||||
@foreach($laborItems as $item)
|
||||
<tr>
|
||||
<td class="text-center">{{ $no++ }}</td>
|
||||
<td class="text-center text-xs">{{ $item->floor_code }}</td>
|
||||
<td>{{ $item->module_name }}</td>
|
||||
<td class="text-center text-xs">{{ $item->specification }}</td>
|
||||
<td class="text-center">{{ $item->unit }}</td>
|
||||
<td class="text-center">{{ number_format((float)$item->quantity, 0) }}</td>
|
||||
<td class="text-right">{{ number_format((float)$item->unit_price) }}</td>
|
||||
<td class="text-right">{{ number_format((float)$item->total_price) }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@endif
|
||||
|
||||
{{-- 설치비 --}}
|
||||
@if($installItems->isNotEmpty())
|
||||
<tr style="background-color: #fff7ed;">
|
||||
<td colspan="8" class="text-sm font-semibold" style="padding: 4px 10px;">[ 설치비 ]</td>
|
||||
</tr>
|
||||
@foreach($installItems as $item)
|
||||
<tr>
|
||||
<td class="text-center">{{ $no++ }}</td>
|
||||
<td class="text-center text-xs">{{ $item->floor_code }}</td>
|
||||
<td>{{ $item->module_name }}</td>
|
||||
<td class="text-center text-xs">{{ $item->specification }}</td>
|
||||
<td class="text-center">{{ $item->unit }}</td>
|
||||
<td class="text-center">{{ number_format((float)$item->quantity, 0) }}</td>
|
||||
<td class="text-right">{{ number_format((float)$item->unit_price) }}</td>
|
||||
<td class="text-right">{{ number_format((float)$item->total_price) }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@endif
|
||||
|
||||
{{-- 기타 --}}
|
||||
@foreach($otherItems as $item)
|
||||
<tr>
|
||||
<td class="text-center">{{ $no++ }}</td>
|
||||
<td class="text-center text-xs">{{ $item->floor_code }}</td>
|
||||
<td>{{ $item->module_name }}</td>
|
||||
<td class="text-center text-xs">{{ $item->specification }}</td>
|
||||
<td class="text-center">{{ $item->unit }}</td>
|
||||
<td class="text-center">{{ number_format((float)$item->quantity, 0) }}</td>
|
||||
<td class="text-right">{{ number_format((float)$item->unit_price) }}</td>
|
||||
<td class="text-right">{{ number_format((float)$item->total_price) }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
<tfoot>
|
||||
{{-- 카테고리별 소계 --}}
|
||||
@if($materialItems->isNotEmpty())
|
||||
<tr>
|
||||
<th colspan="7" class="text-right text-xs">재료비 소계</th>
|
||||
<td class="text-right font-semibold">{{ number_format((int)$materialTotal) }}</td>
|
||||
</tr>
|
||||
@endif
|
||||
@if($laborItems->isNotEmpty())
|
||||
<tr>
|
||||
<th colspan="7" class="text-right text-xs">노무비 소계</th>
|
||||
<td class="text-right font-semibold">{{ number_format((int)$laborTotal) }}</td>
|
||||
</tr>
|
||||
@endif
|
||||
@if($installItems->isNotEmpty())
|
||||
<tr>
|
||||
<th colspan="7" class="text-right text-xs">설치비 소계</th>
|
||||
<td class="text-right font-semibold">{{ number_format((int)$installTotal) }}</td>
|
||||
</tr>
|
||||
@endif
|
||||
<tr>
|
||||
<th colspan="7" class="text-right">소 계</th>
|
||||
<td class="text-right font-bold">{{ number_format($subtotal) }}</td>
|
||||
</tr>
|
||||
@if($discountAmount > 0)
|
||||
<tr>
|
||||
<th colspan="7" class="text-right text-red-600">할인 ({{ $discountRate }}%)</th>
|
||||
<td class="text-right text-red-600">-{{ number_format($discountAmount) }}</td>
|
||||
</tr>
|
||||
@endif
|
||||
<tr>
|
||||
<th colspan="7" class="text-right">부가세 (10%)</th>
|
||||
<td class="text-right">{{ number_format($vatAmount) }}</td>
|
||||
</tr>
|
||||
<tr style="background-color: #f3f4f6;">
|
||||
<th colspan="7" class="text-right text-base">합 계</th>
|
||||
<td class="text-right font-bold text-base">{{ number_format($finalAmount) }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
{{-- 비고 / 조건 --}}
|
||||
<div class="mb-8">
|
||||
<table class="doc-table">
|
||||
<thead><tr><th class="text-left">비 고</th></tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-sm leading-relaxed" style="padding: 12px 16px;">
|
||||
<ol class="list-decimal list-inside space-y-1">
|
||||
<li>상기 금액은 부가가치세 별도입니다.</li>
|
||||
<li>결제 조건: {{ $terms['payment'] ?? '계약 시 50%, 설치 완료 후 50%' }}</li>
|
||||
<li>납기 조건: {{ $terms['delivery'] ?? '계약 후 4주 이내' }}</li>
|
||||
<li>본 견적서의 유효기간: {{ $validUntil }}까지</li>
|
||||
<li>설치 현장 여건에 따라 규격 및 금액이 변경될 수 있습니다.</li>
|
||||
<li>세부 사항은 별도 협의를 통해 조정될 수 있습니다.</li>
|
||||
</ol>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- 서명 --}}
|
||||
<div class="flex justify-end items-center gap-6 mt-12">
|
||||
<div class="text-right">
|
||||
<p class="text-lg font-bold mb-1">(주)코드브릿지엑스</p>
|
||||
<p class="text-sm text-gray-600">대표이사 이 의 찬</p>
|
||||
</div>
|
||||
<div class="seal-box">(인)</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -30,72 +30,63 @@
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
// 견적번호
|
||||
$quotationNo = 'AQ-' . $quotation->created_at->format('Y') . '-' . str_pad($quotation->id, 3, '0', STR_PAD_LEFT);
|
||||
|
||||
// 회사 분석 정보
|
||||
$company = $quotation->analysis_result['company_analysis'] ?? [];
|
||||
|
||||
// 구현 계획
|
||||
$plan = $quotation->quotation_result['implementation_plan'] ?? [];
|
||||
$estimatedMonths = $plan['estimated_months'] ?? null;
|
||||
|
||||
// 금액 계산
|
||||
$devSubtotal = (int) $quotation->total_dev_cost;
|
||||
$monthlySubtotal = (int) $quotation->total_monthly_fee;
|
||||
$devVat = (int) round($devSubtotal * 0.1);
|
||||
$monthlyVat = (int) round($monthlySubtotal * 0.1);
|
||||
$devTotal = $devSubtotal + $devVat;
|
||||
$monthlyTotal = $monthlySubtotal + $monthlyVat;
|
||||
|
||||
// 한글 금액 변환
|
||||
// 한글 금액 변환 함수
|
||||
if (!function_exists('numberToKorean')) {
|
||||
function numberToKorean(int $number): string {
|
||||
if ($number === 0) return '영';
|
||||
$units = ['', '만', '억', '조'];
|
||||
$digits = ['', '일', '이', '삼', '사', '오', '육', '칠', '팔', '구'];
|
||||
$subUnits = ['', '십', '백', '천'];
|
||||
|
||||
$result = '';
|
||||
$unitIndex = 0;
|
||||
while ($number > 0) {
|
||||
$chunk = $number % 10000;
|
||||
if ($chunk > 0) {
|
||||
$chunkStr = '';
|
||||
$subIndex = 0;
|
||||
$temp = $chunk;
|
||||
while ($temp > 0) {
|
||||
$digit = $temp % 10;
|
||||
if ($digit > 0) {
|
||||
$prefix = ($digit === 1 && $subIndex > 0) ? '' : $digits[$digit];
|
||||
$chunkStr = $prefix . $subUnits[$subIndex] . $chunkStr;
|
||||
}
|
||||
$temp = (int)($temp / 10);
|
||||
$subIndex++;
|
||||
}
|
||||
$result = $chunkStr . $units[$unitIndex] . $result;
|
||||
}
|
||||
$number = (int)($number / 10000);
|
||||
$unitIndex++;
|
||||
}
|
||||
return $result;
|
||||
return \App\Services\Rd\AiQuotationService::numberToKorean($number);
|
||||
}
|
||||
}
|
||||
|
||||
$devTotalKorean = numberToKorean($devSubtotal);
|
||||
|
||||
// 필수 → 선택 순으로 정렬된 품목
|
||||
$sortedItems = $quotation->items->sortByDesc('is_required')->values();
|
||||
// 견적 모드 판별
|
||||
$isManufacture = $quotation->isManufacture();
|
||||
|
||||
// 템플릿 (기본값: classic)
|
||||
$template = $template ?? 'classic';
|
||||
$templates = [
|
||||
'classic' => '클래식',
|
||||
'modern' => '모던',
|
||||
'blue' => '블루',
|
||||
'dark' => '다크',
|
||||
'colorful' => '컬러풀',
|
||||
];
|
||||
|
||||
if ($isManufacture) {
|
||||
// 제조 견적서 데이터
|
||||
$options = $quotation->options ?? [];
|
||||
$client = $options['client'] ?? [];
|
||||
$project = $options['project'] ?? [];
|
||||
$pricing = $options['pricing'] ?? [];
|
||||
$terms = $options['terms'] ?? [];
|
||||
|
||||
$quotationNo = $quotation->quote_number ?? 'AQ-'.$quotation->created_at->format('Y').'-'.str_pad($quotation->id, 3, '0', STR_PAD_LEFT);
|
||||
$subtotal = (int) ($pricing['subtotal'] ?? 0);
|
||||
$discountRate = (float) ($pricing['discount_rate'] ?? 0);
|
||||
$discountAmount = (int) ($pricing['discount_amount'] ?? 0);
|
||||
$vatAmount = (int) ($pricing['vat_amount'] ?? 0);
|
||||
$finalAmount = (int) ($pricing['final_amount'] ?? 0);
|
||||
$subtotalKorean = numberToKorean($subtotal);
|
||||
$validUntil = $terms['valid_until'] ?? now()->addDays(30)->format('Y-m-d');
|
||||
|
||||
$templates = [
|
||||
'classic' => '클래식',
|
||||
];
|
||||
} else {
|
||||
// 모듈 추천 견적서 데이터 (기존)
|
||||
$quotationNo = 'AQ-' . $quotation->created_at->format('Y') . '-' . str_pad($quotation->id, 3, '0', STR_PAD_LEFT);
|
||||
$company = $quotation->analysis_result['company_analysis'] ?? [];
|
||||
$plan = $quotation->quotation_result['implementation_plan'] ?? [];
|
||||
$estimatedMonths = $plan['estimated_months'] ?? null;
|
||||
|
||||
$devSubtotal = (int) $quotation->total_dev_cost;
|
||||
$monthlySubtotal = (int) $quotation->total_monthly_fee;
|
||||
$devVat = (int) round($devSubtotal * 0.1);
|
||||
$monthlyVat = (int) round($monthlySubtotal * 0.1);
|
||||
$devTotal = $devSubtotal + $devVat;
|
||||
$monthlyTotal = $monthlySubtotal + $monthlyVat;
|
||||
$devTotalKorean = numberToKorean($devSubtotal);
|
||||
$sortedItems = $quotation->items->sortByDesc('is_required')->values();
|
||||
|
||||
$templates = [
|
||||
'classic' => '클래식',
|
||||
'modern' => '모던',
|
||||
'blue' => '블루',
|
||||
'dark' => '다크',
|
||||
'colorful' => '컬러풀',
|
||||
];
|
||||
}
|
||||
@endphp
|
||||
|
||||
{{-- 템플릿 선택 UI (인쇄 시 숨김) --}}
|
||||
@@ -109,21 +100,45 @@ class="template-card {{ $template === $key ? 'active' : '' }}">
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
{{-- PDF 다운로드 버튼 --}}
|
||||
<div class="text-center mt-3">
|
||||
<button onclick="window.print()" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition text-sm">
|
||||
<i class="ri-printer-line"></i> 인쇄 / PDF 다운로드
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 선택된 템플릿 렌더링 --}}
|
||||
@include('rd.ai-quotation.document-templates.' . $template, [
|
||||
'quotation' => $quotation,
|
||||
'quotationNo' => $quotationNo,
|
||||
'company' => $company,
|
||||
'estimatedMonths' => $estimatedMonths,
|
||||
'devSubtotal' => $devSubtotal,
|
||||
'monthlySubtotal' => $monthlySubtotal,
|
||||
'devVat' => $devVat,
|
||||
'monthlyVat' => $monthlyVat,
|
||||
'devTotal' => $devTotal,
|
||||
'monthlyTotal' => $monthlyTotal,
|
||||
'devTotalKorean' => $devTotalKorean,
|
||||
'sortedItems' => $sortedItems,
|
||||
])
|
||||
@if($isManufacture)
|
||||
@include('rd.ai-quotation.document-templates.manufacture-classic', [
|
||||
'quotation' => $quotation,
|
||||
'quotationNo' => $quotationNo,
|
||||
'client' => $client,
|
||||
'project' => $project,
|
||||
'pricing' => $pricing,
|
||||
'terms' => $terms,
|
||||
'subtotal' => $subtotal,
|
||||
'discountRate' => $discountRate,
|
||||
'discountAmount' => $discountAmount,
|
||||
'vatAmount' => $vatAmount,
|
||||
'finalAmount' => $finalAmount,
|
||||
'subtotalKorean' => $subtotalKorean,
|
||||
'validUntil' => $validUntil,
|
||||
])
|
||||
@else
|
||||
@include('rd.ai-quotation.document-templates.' . $template, [
|
||||
'quotation' => $quotation,
|
||||
'quotationNo' => $quotationNo,
|
||||
'company' => $company,
|
||||
'estimatedMonths' => $estimatedMonths,
|
||||
'devSubtotal' => $devSubtotal,
|
||||
'monthlySubtotal' => $monthlySubtotal,
|
||||
'devVat' => $devVat,
|
||||
'monthlyVat' => $monthlyVat,
|
||||
'devTotal' => $devTotal,
|
||||
'monthlyTotal' => $monthlyTotal,
|
||||
'devTotalKorean' => $devTotalKorean,
|
||||
'sortedItems' => $sortedItems,
|
||||
])
|
||||
@endif
|
||||
@endsection
|
||||
|
||||
403
resources/views/rd/ai-quotation/edit.blade.php
Normal file
403
resources/views/rd/ai-quotation/edit.blade.php
Normal file
@@ -0,0 +1,403 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', '견적서 편집')
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
$options = $quotation->options ?? [];
|
||||
$client = $options['client'] ?? [];
|
||||
$project = $options['project'] ?? [];
|
||||
$pricing = $options['pricing'] ?? [];
|
||||
$terms = $options['terms'] ?? [];
|
||||
@endphp
|
||||
|
||||
<!-- 페이지 헤더 -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800 flex items-center gap-2">
|
||||
<i class="ri-edit-line text-blue-600"></i>
|
||||
견적서 편집
|
||||
<span class="text-base font-normal text-gray-400">{{ $quotation->quote_number }}</span>
|
||||
</h1>
|
||||
<div class="flex gap-2">
|
||||
<a href="{{ route('rd.ai-quotation.show', $quotation->id) }}" class="bg-white hover:bg-gray-100 text-gray-700 px-4 py-2 rounded-lg border transition">
|
||||
<i class="ri-arrow-left-line"></i> 상세보기
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="editForm">
|
||||
<!-- 고객 정보 -->
|
||||
<div class="bg-white rounded-lg shadow-sm mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-100">
|
||||
<h2 class="text-lg font-semibold text-gray-800 flex items-center gap-2">
|
||||
<i class="ri-user-line text-blue-600"></i> 고객 정보
|
||||
</h2>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="grid gap-4" style="grid-template-columns: repeat(2, 1fr);">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">회사명</label>
|
||||
<input type="text" name="client_company" value="{{ $client['company'] ?? '' }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">담당자</label>
|
||||
<input type="text" name="client_contact" value="{{ $client['contact'] ?? '' }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">연락처</label>
|
||||
<input type="text" name="client_phone" value="{{ $client['phone'] ?? '' }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">이메일</label>
|
||||
<input type="email" name="client_email" value="{{ $client['email'] ?? '' }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
</div>
|
||||
<div style="grid-column: span 2;">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">주소</label>
|
||||
<input type="text" name="client_address" value="{{ $client['address'] ?? '' }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 프로젝트 정보 -->
|
||||
<div class="bg-white rounded-lg shadow-sm mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-100">
|
||||
<h2 class="text-lg font-semibold text-gray-800 flex items-center gap-2">
|
||||
<i class="ri-building-line text-green-600"></i> 프로젝트 정보
|
||||
</h2>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="grid gap-4" style="grid-template-columns: repeat(2, 1fr);">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">현장명</label>
|
||||
<input type="text" name="project_name" value="{{ $project['name'] ?? '' }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">위치</label>
|
||||
<input type="text" name="project_location" value="{{ $project['location'] ?? '' }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 품목 편집 -->
|
||||
<div class="bg-white rounded-lg shadow-sm mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center">
|
||||
<h2 class="text-lg font-semibold text-gray-800 flex items-center gap-2">
|
||||
<i class="ri-list-check-2 text-purple-600"></i> 품목 편집
|
||||
</h2>
|
||||
<button type="button" onclick="addItemRow()" class="px-3 py-1.5 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition">
|
||||
<i class="ri-add-line"></i> 행 추가
|
||||
</button>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm" id="itemsTable">
|
||||
<thead class="bg-gray-50 border-b">
|
||||
<tr>
|
||||
<th class="px-3 py-3 text-center text-xs font-medium text-gray-500" style="width: 40px;">No</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500">분류</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500">위치</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500">품목명</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500">규격</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500" style="width: 60px;">단위</th>
|
||||
<th class="px-3 py-3 text-center text-xs font-medium text-gray-500" style="width: 70px;">수량</th>
|
||||
<th class="px-3 py-3 text-right text-xs font-medium text-gray-500" style="width: 120px;">단가</th>
|
||||
<th class="px-3 py-3 text-right text-xs font-medium text-gray-500" style="width: 120px;">금액</th>
|
||||
<th class="px-3 py-3 text-center text-xs font-medium text-gray-500" style="width: 40px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="itemsBody">
|
||||
@foreach($quotation->items as $index => $item)
|
||||
<tr class="item-row border-b border-gray-100" data-index="{{ $index }}">
|
||||
<td class="px-3 py-2 text-center text-gray-500 row-number">{{ $index + 1 }}</td>
|
||||
<td class="px-3 py-2">
|
||||
<select name="items[{{ $index }}][item_category]" class="w-full px-2 py-1.5 border border-gray-300 rounded text-xs">
|
||||
<option value="material" {{ $item->item_category === 'material' ? 'selected' : '' }}>재료비</option>
|
||||
<option value="labor" {{ $item->item_category === 'labor' ? 'selected' : '' }}>노무비</option>
|
||||
<option value="install" {{ $item->item_category === 'install' ? 'selected' : '' }}>설치비</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<input type="text" name="items[{{ $index }}][floor_code]" value="{{ $item->floor_code }}"
|
||||
class="w-full px-2 py-1.5 border border-gray-300 rounded text-xs" placeholder="B1-A01">
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<input type="text" name="items[{{ $index }}][item_name]" value="{{ $item->module_name }}"
|
||||
class="w-full px-2 py-1.5 border border-gray-300 rounded text-xs" required>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<input type="text" name="items[{{ $index }}][specification]" value="{{ $item->specification }}"
|
||||
class="w-full px-2 py-1.5 border border-gray-300 rounded text-xs" placeholder="3000×2500">
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<input type="text" name="items[{{ $index }}][unit]" value="{{ $item->unit ?? 'SET' }}"
|
||||
class="w-full px-2 py-1.5 border border-gray-300 rounded text-xs">
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<input type="number" name="items[{{ $index }}][quantity]" value="{{ (float)$item->quantity }}"
|
||||
class="w-full px-2 py-1.5 border border-gray-300 rounded text-xs text-center item-qty"
|
||||
min="0" step="1" onchange="calcRow(this)">
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<input type="text" name="items[{{ $index }}][unit_price]" value="{{ number_format((float)$item->unit_price) }}"
|
||||
class="w-full px-2 py-1.5 border border-gray-300 rounded text-xs text-right money-input item-price"
|
||||
inputmode="numeric" onfocus="moneyFocus(this)" onblur="moneyBlur(this)" onchange="calcRow(this)">
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<span class="item-total font-medium text-gray-800">{{ number_format((float)$item->total_price) }}</span>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
<button type="button" onclick="removeRow(this)" class="text-red-400 hover:text-red-600 transition">
|
||||
<i class="ri-delete-bin-line"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
<tfoot class="bg-gray-50 border-t-2">
|
||||
<tr>
|
||||
<td colspan="8" class="px-3 py-3 text-right font-bold text-gray-700">소계</td>
|
||||
<td class="px-3 py-3 text-right font-bold text-blue-700" id="subtotalDisplay">{{ number_format((int)($pricing['subtotal'] ?? 0)) }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 가격 조정 + 조건 -->
|
||||
<div class="grid gap-6 mb-6" style="grid-template-columns: 1fr 1fr;">
|
||||
<!-- 가격 조정 -->
|
||||
<div class="bg-white rounded-lg shadow-sm">
|
||||
<div class="px-6 py-4 border-b border-gray-100">
|
||||
<h2 class="text-lg font-semibold text-gray-800 flex items-center gap-2">
|
||||
<i class="ri-calculator-line text-orange-600"></i> 가격 조정
|
||||
</h2>
|
||||
</div>
|
||||
<div class="p-6 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">할인율 (%)</label>
|
||||
<input type="number" name="discount_rate" id="discountRate" value="{{ $pricing['discount_rate'] ?? 0 }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
min="0" max="100" step="0.1" onchange="recalcTotal()">
|
||||
</div>
|
||||
<div class="space-y-2 text-sm border-t pt-4">
|
||||
<div class="flex justify-between"><span class="text-gray-600">소계</span><span id="pricingSubtotal">{{ number_format((int)($pricing['subtotal'] ?? 0)) }}원</span></div>
|
||||
<div class="flex justify-between text-red-600"><span>할인</span><span id="pricingDiscount">-{{ number_format((int)($pricing['discount_amount'] ?? 0)) }}원</span></div>
|
||||
<div class="flex justify-between"><span class="text-gray-600">부가세 (10%)</span><span id="pricingVat">{{ number_format((int)($pricing['vat_amount'] ?? 0)) }}원</span></div>
|
||||
<div class="flex justify-between text-lg border-t pt-2">
|
||||
<span class="font-bold text-blue-700">최종 금액</span>
|
||||
<span class="font-bold text-blue-700" id="pricingFinal">{{ number_format((int)($pricing['final_amount'] ?? 0)) }}원</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 조건 입력 -->
|
||||
<div class="bg-white rounded-lg shadow-sm">
|
||||
<div class="px-6 py-4 border-b border-gray-100">
|
||||
<h2 class="text-lg font-semibold text-gray-800 flex items-center gap-2">
|
||||
<i class="ri-file-list-3-line text-teal-600"></i> 견적 조건
|
||||
</h2>
|
||||
</div>
|
||||
<div class="p-6 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">유효기간</label>
|
||||
<input type="date" name="terms_valid_until" value="{{ $terms['valid_until'] ?? now()->addDays(30)->format('Y-m-d') }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">결제조건</label>
|
||||
<input type="text" name="terms_payment" value="{{ $terms['payment'] ?? '계약 시 50%, 설치 완료 후 50%' }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">납기조건</label>
|
||||
<input type="text" name="terms_delivery" value="{{ $terms['delivery'] ?? '계약 후 4주 이내' }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 저장 버튼 -->
|
||||
<div class="flex justify-end gap-3">
|
||||
<a href="{{ route('rd.ai-quotation.show', $quotation->id) }}" class="px-6 py-2.5 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition">
|
||||
취소
|
||||
</a>
|
||||
<button type="submit" id="saveBtn" class="px-6 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition flex items-center gap-2">
|
||||
<i class="ri-save-line"></i> 저장
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
let rowCounter = {{ $quotation->items->count() }};
|
||||
|
||||
function moneyFocus(el) { el.value = el.value.replace(/,/g, ''); }
|
||||
function moneyBlur(el) {
|
||||
const val = parseInt(el.value) || 0;
|
||||
el.value = val.toLocaleString();
|
||||
}
|
||||
function parseMoneyValue(el) { return parseInt(String(el.value).replace(/,/g, '')) || 0; }
|
||||
|
||||
function calcRow(el) {
|
||||
const row = el.closest('tr');
|
||||
const qty = parseFloat(row.querySelector('.item-qty').value) || 0;
|
||||
const priceEl = row.querySelector('.item-price');
|
||||
const price = parseMoneyValue(priceEl);
|
||||
const total = Math.round(qty * price);
|
||||
row.querySelector('.item-total').textContent = total.toLocaleString();
|
||||
recalcTotal();
|
||||
}
|
||||
|
||||
function recalcTotal() {
|
||||
let subtotal = 0;
|
||||
document.querySelectorAll('.item-row').forEach(row => {
|
||||
const text = row.querySelector('.item-total')?.textContent || '0';
|
||||
subtotal += parseInt(text.replace(/,/g, '')) || 0;
|
||||
});
|
||||
|
||||
const discountRate = parseFloat(document.getElementById('discountRate').value) || 0;
|
||||
const discountAmount = Math.round(subtotal * discountRate / 100);
|
||||
const afterDiscount = subtotal - discountAmount;
|
||||
const vat = Math.round(afterDiscount * 0.1);
|
||||
const final_amount = afterDiscount + vat;
|
||||
|
||||
document.getElementById('subtotalDisplay').textContent = subtotal.toLocaleString();
|
||||
document.getElementById('pricingSubtotal').textContent = subtotal.toLocaleString() + '원';
|
||||
document.getElementById('pricingDiscount').textContent = '-' + discountAmount.toLocaleString() + '원';
|
||||
document.getElementById('pricingVat').textContent = vat.toLocaleString() + '원';
|
||||
document.getElementById('pricingFinal').textContent = final_amount.toLocaleString() + '원';
|
||||
}
|
||||
|
||||
function addItemRow() {
|
||||
const idx = rowCounter++;
|
||||
const tbody = document.getElementById('itemsBody');
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = 'item-row border-b border-gray-100';
|
||||
tr.dataset.index = idx;
|
||||
tr.innerHTML = `
|
||||
<td class="px-3 py-2 text-center text-gray-500 row-number"></td>
|
||||
<td class="px-3 py-2">
|
||||
<select name="items[${idx}][item_category]" class="w-full px-2 py-1.5 border border-gray-300 rounded text-xs">
|
||||
<option value="material">재료비</option>
|
||||
<option value="labor">노무비</option>
|
||||
<option value="install">설치비</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="px-3 py-2"><input type="text" name="items[${idx}][floor_code]" class="w-full px-2 py-1.5 border border-gray-300 rounded text-xs" placeholder="B1-A01"></td>
|
||||
<td class="px-3 py-2"><input type="text" name="items[${idx}][item_name]" class="w-full px-2 py-1.5 border border-gray-300 rounded text-xs" required></td>
|
||||
<td class="px-3 py-2"><input type="text" name="items[${idx}][specification]" class="w-full px-2 py-1.5 border border-gray-300 rounded text-xs" placeholder="3000×2500"></td>
|
||||
<td class="px-3 py-2"><input type="text" name="items[${idx}][unit]" value="SET" class="w-full px-2 py-1.5 border border-gray-300 rounded text-xs"></td>
|
||||
<td class="px-3 py-2"><input type="number" name="items[${idx}][quantity]" value="1" class="w-full px-2 py-1.5 border border-gray-300 rounded text-xs text-center item-qty" min="0" step="1" onchange="calcRow(this)"></td>
|
||||
<td class="px-3 py-2"><input type="text" name="items[${idx}][unit_price]" value="0" class="w-full px-2 py-1.5 border border-gray-300 rounded text-xs text-right money-input item-price" inputmode="numeric" onfocus="moneyFocus(this)" onblur="moneyBlur(this)" onchange="calcRow(this)"></td>
|
||||
<td class="px-3 py-2"><span class="item-total font-medium text-gray-800">0</span></td>
|
||||
<td class="px-3 py-2 text-center"><button type="button" onclick="removeRow(this)" class="text-red-400 hover:text-red-600 transition"><i class="ri-delete-bin-line"></i></button></td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
renumberRows();
|
||||
}
|
||||
|
||||
function removeRow(btn) {
|
||||
if (document.querySelectorAll('.item-row').length <= 1) {
|
||||
alert('최소 1개의 품목이 필요합니다.');
|
||||
return;
|
||||
}
|
||||
btn.closest('tr').remove();
|
||||
renumberRows();
|
||||
recalcTotal();
|
||||
}
|
||||
|
||||
function renumberRows() {
|
||||
document.querySelectorAll('.item-row').forEach((row, idx) => {
|
||||
row.querySelector('.row-number').textContent = idx + 1;
|
||||
});
|
||||
}
|
||||
|
||||
// 폼 제출
|
||||
document.getElementById('editForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('saveBtn');
|
||||
const original = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="ri-loader-4-line animate-spin"></i> 저장중...';
|
||||
|
||||
// 데이터 수집
|
||||
const items = [];
|
||||
document.querySelectorAll('.item-row').forEach(row => {
|
||||
const inputs = row.querySelectorAll('input, select');
|
||||
const item = {};
|
||||
inputs.forEach(input => {
|
||||
const name = input.name;
|
||||
if (!name) return;
|
||||
const key = name.replace(/items\[\d+\]\[/, '').replace(']', '');
|
||||
let val = input.value;
|
||||
if (key === 'unit_price') val = String(val).replace(/,/g, '');
|
||||
item[key] = val;
|
||||
});
|
||||
items.push(item);
|
||||
});
|
||||
|
||||
const payload = {
|
||||
client: {
|
||||
company: document.querySelector('[name="client_company"]').value,
|
||||
contact: document.querySelector('[name="client_contact"]').value,
|
||||
phone: document.querySelector('[name="client_phone"]').value,
|
||||
email: document.querySelector('[name="client_email"]').value,
|
||||
address: document.querySelector('[name="client_address"]').value,
|
||||
},
|
||||
project: {
|
||||
name: document.querySelector('[name="project_name"]').value,
|
||||
location: document.querySelector('[name="project_location"]').value,
|
||||
},
|
||||
terms: {
|
||||
valid_until: document.querySelector('[name="terms_valid_until"]').value,
|
||||
payment: document.querySelector('[name="terms_payment"]').value,
|
||||
delivery: document.querySelector('[name="terms_delivery"]').value,
|
||||
},
|
||||
discount_rate: parseFloat(document.getElementById('discountRate').value) || 0,
|
||||
items: items,
|
||||
};
|
||||
|
||||
try {
|
||||
const token = document.querySelector('meta[name="api-token"]')?.content
|
||||
|| sessionStorage.getItem('api_token') || '';
|
||||
|
||||
const response = await fetch('{{ url("/api/admin/rd/ai-quotation/{$quotation->id}") }}', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || '',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
window.location.href = '{{ route("rd.ai-quotation.show", $quotation->id) }}';
|
||||
} else {
|
||||
alert(result.message || '저장에 실패했습니다.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('저장 실패:', err);
|
||||
alert('서버 통신 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = original;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@@ -11,12 +11,21 @@
|
||||
{{ $quotation->title }}
|
||||
</h1>
|
||||
<span class="badge {{ $quotation->status_color }}">{{ $quotation->status_label }}</span>
|
||||
@if($quotation->isManufacture())
|
||||
<span class="px-2 py-0.5 bg-blue-100 text-blue-700 text-xs rounded-full font-medium">제조 견적</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<a href="{{ route('rd.ai-quotation.index') }}" class="bg-white hover:bg-gray-100 text-gray-700 px-4 py-2 rounded-lg border transition">
|
||||
<i class="ri-arrow-left-line"></i> 목록
|
||||
</a>
|
||||
@if($quotation->isCompleted())
|
||||
@if($quotation->isManufacture())
|
||||
<a href="{{ route('rd.ai-quotation.edit', $quotation->id) }}"
|
||||
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition">
|
||||
<i class="ri-edit-line"></i> 견적서 편집
|
||||
</a>
|
||||
@endif
|
||||
<a href="{{ route('rd.ai-quotation.document', $quotation->id) }}" target="_blank"
|
||||
class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg transition">
|
||||
<i class="ri-file-text-line"></i> 견적서 보기
|
||||
@@ -38,10 +47,21 @@ class="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-lg transit
|
||||
<p class="text-xs text-gray-500 mb-1">AI Provider</p>
|
||||
<p class="font-medium text-gray-800">{{ strtoupper($quotation->ai_provider) }}{{ $quotation->ai_model ? ' ('.$quotation->ai_model.')' : '' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 mb-1">입력 유형</p>
|
||||
<p class="font-medium text-gray-800">{{ ['text' => '텍스트', 'voice' => '음성', 'document' => '문서'][$quotation->input_type] ?? $quotation->input_type }}</p>
|
||||
</div>
|
||||
@if($quotation->isManufacture())
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 mb-1">견적번호</p>
|
||||
<p class="font-medium text-gray-800 font-mono">{{ $quotation->quote_number ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 mb-1">제품 카테고리</p>
|
||||
<p class="font-medium text-gray-800">{{ $quotation->product_category === 'STEEL' ? '철재' : '방화스크린' }}</p>
|
||||
</div>
|
||||
@else
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 mb-1">입력 유형</p>
|
||||
<p class="font-medium text-gray-800">{{ ['text' => '텍스트', 'voice' => '음성', 'document' => '문서'][$quotation->input_type] ?? $quotation->input_type }}</p>
|
||||
</div>
|
||||
@endif
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 mb-1">요청자</p>
|
||||
<p class="font-medium text-gray-800">{{ $quotation->creator?->name ?? '-' }}</p>
|
||||
@@ -54,46 +74,297 @@ class="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-lg transit
|
||||
</div>
|
||||
|
||||
@if($quotation->isCompleted())
|
||||
<!-- 업무 분석 결과 -->
|
||||
@if($quotation->analysis_result)
|
||||
@php $analysis = $quotation->analysis_result; @endphp
|
||||
@if($quotation->isManufacture())
|
||||
{{-- ============================================ --}}
|
||||
{{-- 제조 견적 모드 --}}
|
||||
{{-- ============================================ --}}
|
||||
|
||||
@php
|
||||
$options = $quotation->options ?? [];
|
||||
$client = $options['client'] ?? [];
|
||||
$project = $options['project'] ?? [];
|
||||
$pricing = $options['pricing'] ?? [];
|
||||
$terms = $options['terms'] ?? [];
|
||||
$analysis = $quotation->analysis_result ?? [];
|
||||
$productSpecs = $analysis['product_specs'] ?? [];
|
||||
@endphp
|
||||
|
||||
<!-- 고객 정보 -->
|
||||
@if(!empty($client))
|
||||
<div class="bg-white rounded-lg shadow-sm mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-100">
|
||||
<h2 class="text-lg font-semibold text-gray-800 flex items-center gap-2">
|
||||
<i class="ri-search-eye-line text-blue-600"></i> AI 업무 분석 결과
|
||||
<i class="ri-user-line text-blue-600"></i> 고객 정보
|
||||
</h2>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<!-- 기업 분석 -->
|
||||
@if(isset($analysis['company_analysis']))
|
||||
@php $company = $analysis['company_analysis']; @endphp
|
||||
<div class="flex flex-wrap gap-4 mb-6 pb-6 border-b border-gray-100">
|
||||
<div class="px-4 py-2 bg-blue-50 rounded-lg">
|
||||
<span class="text-xs text-blue-500 block">업종</span>
|
||||
<span class="font-semibold text-blue-800">{{ $company['industry'] ?? '-' }}</span>
|
||||
</div>
|
||||
<div class="px-4 py-2 bg-green-50 rounded-lg">
|
||||
<span class="text-xs text-green-500 block">규모</span>
|
||||
<span class="font-semibold text-green-800">{{ $company['scale'] ?? '-' }}</span>
|
||||
</div>
|
||||
<div class="px-4 py-2 bg-purple-50 rounded-lg">
|
||||
<span class="text-xs text-purple-500 block">디지털화 수준</span>
|
||||
<span class="font-semibold text-purple-800">{{ $company['digitalization_level'] ?? '-' }}</span>
|
||||
</div>
|
||||
@if(!empty($company['current_systems']))
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 mb-1">회사명</p>
|
||||
<p class="font-medium text-gray-800">{{ $client['company'] ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 mb-1">담당자</p>
|
||||
<p class="font-medium text-gray-800">{{ $client['contact'] ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 mb-1">연락처</p>
|
||||
<p class="font-medium text-gray-800">{{ $client['phone'] ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 mb-1">이메일</p>
|
||||
<p class="font-medium text-gray-800">{{ $client['email'] ?? '-' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- 프로젝트 분석 -->
|
||||
@if(!empty($project))
|
||||
<div class="bg-white rounded-lg shadow-sm mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-100">
|
||||
<h2 class="text-lg font-semibold text-gray-800 flex items-center gap-2">
|
||||
<i class="ri-building-line text-green-600"></i> 프로젝트 분석
|
||||
</h2>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<div class="px-4 py-2 bg-green-50 rounded-lg">
|
||||
<span class="text-xs text-green-500 block">현장명</span>
|
||||
<span class="font-semibold text-green-800">{{ $project['name'] ?? '-' }}</span>
|
||||
</div>
|
||||
<div class="px-4 py-2 bg-blue-50 rounded-lg">
|
||||
<span class="text-xs text-blue-500 block">건물유형</span>
|
||||
<span class="font-semibold text-blue-800">{{ $project['building_type'] ?? '-' }}</span>
|
||||
</div>
|
||||
@if(!empty($project['location']))
|
||||
<div class="px-4 py-2 bg-gray-50 rounded-lg">
|
||||
<span class="text-xs text-gray-500 block">위치</span>
|
||||
<span class="font-semibold text-gray-800">{{ $project['location'] }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- 위치별 사양 테이블 -->
|
||||
@if(!empty($productSpecs))
|
||||
<div class="bg-white rounded-lg shadow-sm mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-100">
|
||||
<h2 class="text-lg font-semibold text-gray-800 flex items-center gap-2">
|
||||
<i class="ri-ruler-line text-indigo-600"></i> 위치별 제품 사양 (AI 추출)
|
||||
</h2>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 border-b">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">위치</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">유형</th>
|
||||
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">규격 (W×H)</th>
|
||||
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">수량</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">가이드레일</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">모터</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">비고</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-50">
|
||||
@foreach($productSpecs as $spec)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-3">
|
||||
<div class="font-medium text-gray-800">{{ $spec['floor_name'] ?? '-' }}</div>
|
||||
<div class="text-xs text-gray-400 font-mono">{{ $spec['floor_code'] ?? '' }}</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<span class="px-2 py-0.5 bg-blue-100 text-blue-700 text-xs rounded-full">{{ $spec['product_type'] ?? '-' }}</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center font-mono">{{ ($spec['width_mm'] ?? 0) }}×{{ ($spec['height_mm'] ?? 0) }}</td>
|
||||
<td class="px-4 py-3 text-center font-medium">{{ $spec['quantity'] ?? 1 }}</td>
|
||||
<td class="px-4 py-3 text-gray-600">{{ $spec['guide_rail'] ?? '-' }}</td>
|
||||
<td class="px-4 py-3 text-gray-600">{{ $spec['motor'] ?? '-' }}</td>
|
||||
<td class="px-4 py-3 text-xs text-gray-500">{{ $spec['note'] ?? '' }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- 견적 품목 테이블 -->
|
||||
<div class="bg-white rounded-lg shadow-sm mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-100">
|
||||
<h2 class="text-lg font-semibold text-gray-800 flex items-center gap-2">
|
||||
<i class="ri-price-tag-3-line text-green-600"></i> 견적 품목
|
||||
</h2>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
@php
|
||||
$materialItems = $quotation->items->where('item_category', 'material');
|
||||
$laborItems = $quotation->items->where('item_category', 'labor');
|
||||
$installItems = $quotation->items->where('item_category', 'install');
|
||||
$otherItems = $quotation->items->whereNotIn('item_category', ['material', 'labor', 'install']);
|
||||
@endphp
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 border-b">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase" style="width: 40px;">No</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">위치</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">품목</th>
|
||||
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">규격</th>
|
||||
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">수량</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">단가</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">금액</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-50">
|
||||
@php $no = 1; @endphp
|
||||
|
||||
{{-- 재료비 --}}
|
||||
@if($materialItems->isNotEmpty())
|
||||
<tr class="bg-blue-50">
|
||||
<td colspan="7" class="px-4 py-2 text-xs font-semibold text-blue-700">재료비</td>
|
||||
</tr>
|
||||
@foreach($materialItems as $item)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-3 text-center text-gray-500">{{ $no++ }}</td>
|
||||
<td class="px-4 py-3 text-xs text-gray-500 font-mono">{{ $item->floor_code }}</td>
|
||||
<td class="px-4 py-3 font-medium text-gray-800">{{ $item->module_name }}</td>
|
||||
<td class="px-4 py-3 text-center text-gray-600 font-mono text-xs">{{ $item->specification }}</td>
|
||||
<td class="px-4 py-3 text-center text-gray-600">{{ number_format((float)$item->quantity, 0) }} {{ $item->unit }}</td>
|
||||
<td class="px-4 py-3 text-right text-gray-600">{{ number_format((float)$item->unit_price) }}</td>
|
||||
<td class="px-4 py-3 text-right font-medium text-gray-800">{{ number_format((float)$item->total_price) }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@endif
|
||||
|
||||
{{-- 노무비 --}}
|
||||
@if($laborItems->isNotEmpty())
|
||||
<tr class="bg-green-50">
|
||||
<td colspan="7" class="px-4 py-2 text-xs font-semibold text-green-700">노무비</td>
|
||||
</tr>
|
||||
@foreach($laborItems as $item)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-3 text-center text-gray-500">{{ $no++ }}</td>
|
||||
<td class="px-4 py-3 text-xs text-gray-500 font-mono">{{ $item->floor_code }}</td>
|
||||
<td class="px-4 py-3 font-medium text-gray-800">{{ $item->module_name }}</td>
|
||||
<td class="px-4 py-3 text-center text-gray-600 font-mono text-xs">{{ $item->specification }}</td>
|
||||
<td class="px-4 py-3 text-center text-gray-600">{{ number_format((float)$item->quantity, 0) }} {{ $item->unit }}</td>
|
||||
<td class="px-4 py-3 text-right text-gray-600">{{ number_format((float)$item->unit_price) }}</td>
|
||||
<td class="px-4 py-3 text-right font-medium text-gray-800">{{ number_format((float)$item->total_price) }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@endif
|
||||
|
||||
{{-- 설치비 --}}
|
||||
@if($installItems->isNotEmpty())
|
||||
<tr class="bg-orange-50">
|
||||
<td colspan="7" class="px-4 py-2 text-xs font-semibold text-orange-700">설치비</td>
|
||||
</tr>
|
||||
@foreach($installItems as $item)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-3 text-center text-gray-500">{{ $no++ }}</td>
|
||||
<td class="px-4 py-3 text-xs text-gray-500 font-mono">{{ $item->floor_code }}</td>
|
||||
<td class="px-4 py-3 font-medium text-gray-800">{{ $item->module_name }}</td>
|
||||
<td class="px-4 py-3 text-center text-gray-600 font-mono text-xs">{{ $item->specification }}</td>
|
||||
<td class="px-4 py-3 text-center text-gray-600">{{ number_format((float)$item->quantity, 0) }} {{ $item->unit }}</td>
|
||||
<td class="px-4 py-3 text-right text-gray-600">{{ number_format((float)$item->unit_price) }}</td>
|
||||
<td class="px-4 py-3 text-right font-medium text-gray-800">{{ number_format((float)$item->total_price) }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@endif
|
||||
|
||||
{{-- 기타 --}}
|
||||
@foreach($otherItems as $item)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-3 text-center text-gray-500">{{ $no++ }}</td>
|
||||
<td class="px-4 py-3 text-xs text-gray-500 font-mono">{{ $item->floor_code }}</td>
|
||||
<td class="px-4 py-3 font-medium text-gray-800">{{ $item->module_name }}</td>
|
||||
<td class="px-4 py-3 text-center text-gray-600 font-mono text-xs">{{ $item->specification }}</td>
|
||||
<td class="px-4 py-3 text-center text-gray-600">{{ number_format((float)$item->quantity, 0) }} {{ $item->unit }}</td>
|
||||
<td class="px-4 py-3 text-right text-gray-600">{{ number_format((float)$item->unit_price) }}</td>
|
||||
<td class="px-4 py-3 text-right font-medium text-gray-800">{{ number_format((float)$item->total_price) }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 금액 요약 -->
|
||||
<div class="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-lg p-6 mb-6">
|
||||
<h3 class="font-semibold text-gray-800 mb-4 flex items-center gap-2">
|
||||
<i class="ri-money-cny-circle-line text-blue-600"></i> 금액 요약
|
||||
</h3>
|
||||
<div class="grid gap-3" style="grid-template-columns: 1fr 1fr;">
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="flex justify-between"><span class="text-gray-600">재료비</span><span class="font-medium">{{ number_format((int)($pricing['material_cost'] ?? 0)) }}원</span></div>
|
||||
<div class="flex justify-between"><span class="text-gray-600">노무비</span><span class="font-medium">{{ number_format((int)($pricing['labor_cost'] ?? 0)) }}원</span></div>
|
||||
<div class="flex justify-between"><span class="text-gray-600">설치비</span><span class="font-medium">{{ number_format((int)($pricing['install_cost'] ?? 0)) }}원</span></div>
|
||||
<div class="flex justify-between border-t pt-2"><span class="font-semibold">소계</span><span class="font-bold">{{ number_format((int)($pricing['subtotal'] ?? 0)) }}원</span></div>
|
||||
</div>
|
||||
<div class="space-y-2 text-sm">
|
||||
@if(($pricing['discount_rate'] ?? 0) > 0)
|
||||
<div class="flex justify-between"><span class="text-red-600">할인 ({{ $pricing['discount_rate'] }}%)</span><span class="font-medium text-red-600">-{{ number_format((int)($pricing['discount_amount'] ?? 0)) }}원</span></div>
|
||||
@endif
|
||||
<div class="flex justify-between"><span class="text-gray-600">부가세 (10%)</span><span class="font-medium">{{ number_format((int)($pricing['vat_amount'] ?? 0)) }}원</span></div>
|
||||
<div class="flex justify-between border-t pt-2 text-lg">
|
||||
<span class="font-bold text-blue-700">최종 금액</span>
|
||||
<span class="font-bold text-blue-700">{{ number_format((int)($pricing['final_amount'] ?? 0)) }}원</span>
|
||||
</div>
|
||||
@php
|
||||
$koreanAmount = \App\Services\Rd\AiQuotationService::numberToKorean((int)($pricing['subtotal'] ?? 0));
|
||||
@endphp
|
||||
<div class="text-xs text-gray-500 text-right">금 {{ $koreanAmount }}원정 (VAT 별도)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@else
|
||||
{{-- ============================================ --}}
|
||||
{{-- 모듈 추천 모드 (기존) --}}
|
||||
{{-- ============================================ --}}
|
||||
|
||||
<!-- 업무 분석 결과 -->
|
||||
@if($quotation->analysis_result)
|
||||
@php $analysis = $quotation->analysis_result; @endphp
|
||||
<div class="bg-white rounded-lg shadow-sm mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-100">
|
||||
<h2 class="text-lg font-semibold text-gray-800 flex items-center gap-2">
|
||||
<i class="ri-search-eye-line text-blue-600"></i> AI 업무 분석 결과
|
||||
</h2>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
@if(isset($analysis['company_analysis']))
|
||||
@php $company = $analysis['company_analysis']; @endphp
|
||||
<div class="flex flex-wrap gap-4 mb-6 pb-6 border-b border-gray-100">
|
||||
<div class="px-4 py-2 bg-blue-50 rounded-lg">
|
||||
<span class="text-xs text-blue-500 block">업종</span>
|
||||
<span class="font-semibold text-blue-800">{{ $company['industry'] ?? '-' }}</span>
|
||||
</div>
|
||||
<div class="px-4 py-2 bg-green-50 rounded-lg">
|
||||
<span class="text-xs text-green-500 block">규모</span>
|
||||
<span class="font-semibold text-green-800">{{ $company['scale'] ?? '-' }}</span>
|
||||
</div>
|
||||
<div class="px-4 py-2 bg-purple-50 rounded-lg">
|
||||
<span class="text-xs text-purple-500 block">디지털화 수준</span>
|
||||
<span class="font-semibold text-purple-800">{{ $company['digitalization_level'] ?? '-' }}</span>
|
||||
</div>
|
||||
@if(!empty($company['current_systems']))
|
||||
<div class="px-4 py-2 bg-gray-50 rounded-lg">
|
||||
<span class="text-xs text-gray-500 block">현재 시스템</span>
|
||||
<span class="font-semibold text-gray-800">{{ implode(', ', $company['current_systems']) }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- 업무 영역별 분석 -->
|
||||
@if(!empty($analysis['business_domains']))
|
||||
<h3 class="text-sm font-semibold text-gray-600 uppercase mb-3">업무 영역 분석</h3>
|
||||
<div class="space-y-4">
|
||||
@foreach($analysis['business_domains'] as $domain)
|
||||
@if(!empty($analysis['business_domains']))
|
||||
<h3 class="text-sm font-semibold text-gray-600 uppercase mb-3">업무 영역 분석</h3>
|
||||
<div class="space-y-4">
|
||||
@foreach($analysis['business_domains'] as $domain)
|
||||
<div class="border border-gray-200 rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h4 class="font-semibold text-gray-800">{{ $domain['domain'] ?? '' }}</h4>
|
||||
@@ -109,47 +380,47 @@ class="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-lg transit
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 mb-2">{{ $domain['current_process'] ?? '' }}</p>
|
||||
@if(!empty($domain['pain_points']))
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
@foreach($domain['pain_points'] as $point)
|
||||
<span class="px-2 py-0.5 bg-red-50 text-red-600 text-xs rounded">{{ $point }}</span>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
@foreach($domain['pain_points'] as $point)
|
||||
<span class="px-2 py-0.5 bg-red-50 text-red-600 text-xs rounded">{{ $point }}</span>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
@if(!empty($domain['matched_modules']))
|
||||
<div class="flex flex-wrap gap-1.5 mt-2">
|
||||
@foreach($domain['matched_modules'] as $mod)
|
||||
<span class="px-2 py-0.5 bg-purple-50 text-purple-600 text-xs rounded font-mono">{{ $mod }}</span>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1.5 mt-2">
|
||||
@foreach($domain['matched_modules'] as $mod)
|
||||
<span class="px-2 py-0.5 bg-purple-50 text-purple-600 text-xs rounded font-mono">{{ $mod }}</span>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
<!-- 추천 모듈 + 견적 -->
|
||||
<div class="bg-white rounded-lg shadow-sm mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-100">
|
||||
<h2 class="text-lg font-semibold text-gray-800 flex items-center gap-2">
|
||||
<i class="ri-price-tag-3-line text-green-600"></i> 추천 모듈 및 견적
|
||||
</h2>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 border-b">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">구분</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">모듈</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">추천 근거</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">개발비</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">월 구독료</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-50">
|
||||
@foreach($quotation->items as $item)
|
||||
<!-- 추천 모듈 + 견적 -->
|
||||
<div class="bg-white rounded-lg shadow-sm mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-100">
|
||||
<h2 class="text-lg font-semibold text-gray-800 flex items-center gap-2">
|
||||
<i class="ri-price-tag-3-line text-green-600"></i> 추천 모듈 및 견적
|
||||
</h2>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 border-b">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">구분</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">모듈</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">추천 근거</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">개발비</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">월 구독료</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-50">
|
||||
@foreach($quotation->items as $item)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-3">
|
||||
@if($item->is_required)
|
||||
@@ -168,69 +439,69 @@ class="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-lg transit
|
||||
<td class="px-4 py-3 text-right font-medium text-gray-800">{{ number_format((int)$item->dev_cost) }}원</td>
|
||||
<td class="px-4 py-3 text-right font-medium text-gray-800">{{ number_format((int)$item->monthly_fee) }}원</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
<tfoot class="bg-purple-50 border-t-2 border-purple-200">
|
||||
<tr>
|
||||
<td colspan="3" class="px-4 py-3 text-right font-bold text-gray-800">합계</td>
|
||||
<td class="px-4 py-3 text-right font-bold text-purple-700 text-base">{{ number_format((int)$quotation->total_dev_cost) }}원</td>
|
||||
<td class="px-4 py-3 text-right font-bold text-purple-700 text-base">{{ number_format((int)$quotation->total_monthly_fee) }}원/월</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 구현 계획 (AI 생성) -->
|
||||
@if(!empty($quotation->quotation_result['implementation_plan']))
|
||||
@php $plan = $quotation->quotation_result['implementation_plan']; @endphp
|
||||
<div class="bg-white rounded-lg shadow-sm mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-100">
|
||||
<h2 class="text-lg font-semibold text-gray-800 flex items-center gap-2">
|
||||
<i class="ri-calendar-schedule-line text-indigo-600"></i> 구현 계획 (AI 추천)
|
||||
</h2>
|
||||
@endforeach
|
||||
</tbody>
|
||||
<tfoot class="bg-purple-50 border-t-2 border-purple-200">
|
||||
<tr>
|
||||
<td colspan="3" class="px-4 py-3 text-right font-bold text-gray-800">합계</td>
|
||||
<td class="px-4 py-3 text-right font-bold text-purple-700 text-base">{{ number_format((int)$quotation->total_dev_cost) }}원</td>
|
||||
<td class="px-4 py-3 text-right font-bold text-purple-700 text-base">{{ number_format((int)$quotation->total_monthly_fee) }}원/월</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<p class="text-sm text-gray-600 mb-4">예상 기간: <span class="font-semibold">{{ $plan['estimated_months'] ?? '?' }}개월</span></p>
|
||||
@if(!empty($plan['phases']))
|
||||
</div>
|
||||
|
||||
<!-- 구현 계획 -->
|
||||
@if(!empty($quotation->quotation_result['implementation_plan']))
|
||||
@php $plan = $quotation->quotation_result['implementation_plan']; @endphp
|
||||
<div class="bg-white rounded-lg shadow-sm mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-100">
|
||||
<h2 class="text-lg font-semibold text-gray-800 flex items-center gap-2">
|
||||
<i class="ri-calendar-schedule-line text-indigo-600"></i> 구현 계획 (AI 추천)
|
||||
</h2>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<p class="text-sm text-gray-600 mb-4">예상 기간: <span class="font-semibold">{{ $plan['estimated_months'] ?? '?' }}개월</span></p>
|
||||
@if(!empty($plan['phases']))
|
||||
<div class="space-y-3">
|
||||
@foreach($plan['phases'] as $phase)
|
||||
<div class="flex items-center gap-4 p-3 bg-gray-50 rounded-lg">
|
||||
<div class="w-10 h-10 bg-indigo-100 text-indigo-700 rounded-full flex items-center justify-center font-bold shrink-0">
|
||||
{{ $phase['phase'] ?? '' }}
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium text-gray-800">{{ $phase['name'] ?? '' }}</p>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<span class="text-xs text-gray-500">{{ $phase['duration_weeks'] ?? '?' }}주</span>
|
||||
@if(!empty($phase['modules']))
|
||||
<span class="text-xs text-gray-400">|</span>
|
||||
@foreach($phase['modules'] as $mod)
|
||||
<span class="px-1.5 py-0.5 bg-indigo-50 text-indigo-600 text-xs rounded font-mono">{{ $mod }}</span>
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex items-center gap-4 p-3 bg-gray-50 rounded-lg">
|
||||
<div class="w-10 h-10 bg-indigo-100 text-indigo-700 rounded-full flex items-center justify-center font-bold shrink-0">
|
||||
{{ $phase['phase'] ?? '' }}
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium text-gray-800">{{ $phase['name'] ?? '' }}</p>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<span class="text-xs text-gray-500">{{ $phase['duration_weeks'] ?? '?' }}주</span>
|
||||
@if(!empty($phase['modules']))
|
||||
<span class="text-xs text-gray-400">|</span>
|
||||
@foreach($phase['modules'] as $mod)
|
||||
<span class="px-1.5 py-0.5 bg-indigo-50 text-indigo-600 text-xs rounded font-mono">{{ $mod }}</span>
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
<!-- 분석 요약 -->
|
||||
@if(!empty($quotation->quotation_result['analysis_summary']))
|
||||
<!-- 분석 요약 -->
|
||||
@if(!empty($quotation->quotation_result['analysis_summary']))
|
||||
<div class="bg-gradient-to-r from-purple-50 to-indigo-50 rounded-lg p-6 mb-6">
|
||||
<h3 class="font-semibold text-gray-800 mb-2 flex items-center gap-2">
|
||||
<i class="ri-lightbulb-line text-yellow-500"></i> AI 분석 요약
|
||||
</h3>
|
||||
<p class="text-gray-700">{{ $quotation->quotation_result['analysis_summary'] }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
@elseif($quotation->status === 'failed')
|
||||
<!-- 실패 상태 -->
|
||||
<div class="bg-red-50 border border-red-200 rounded-lg p-6 mb-6">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<i class="ri-error-warning-line text-2xl text-red-500"></i>
|
||||
@@ -240,7 +511,6 @@ class="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-lg transit
|
||||
</div>
|
||||
|
||||
@elseif($quotation->isProcessing())
|
||||
<!-- 분석중 상태 -->
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-6 text-center">
|
||||
<i class="ri-loader-4-line text-4xl text-blue-500 animate-spin mb-2 block"></i>
|
||||
<h2 class="text-lg font-semibold text-blue-800">AI 분석 진행중...</h2>
|
||||
|
||||
@@ -612,6 +612,7 @@
|
||||
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::put('/{id}', [\App\Http\Controllers\Api\Admin\Rd\AiQuotationController::class, 'update'])->name('update');
|
||||
Route::post('/{id}/analyze', [\App\Http\Controllers\Api\Admin\Rd\AiQuotationController::class, 'analyze'])->name('analyze');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -376,6 +376,7 @@
|
||||
Route::get('/ai-quotation', [RdController::class, 'quotations'])->name('ai-quotation.index');
|
||||
Route::get('/ai-quotation/create', [RdController::class, 'createQuotation'])->name('ai-quotation.create');
|
||||
Route::get('/ai-quotation/{id}/document', [RdController::class, 'documentQuotation'])->name('ai-quotation.document');
|
||||
Route::get('/ai-quotation/{id}/edit', [RdController::class, 'editQuotation'])->name('ai-quotation.edit');
|
||||
Route::get('/ai-quotation/{id}', [RdController::class, 'showQuotation'])->name('ai-quotation.show');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user