feat: [ai-quotation] 제조업 표준 견적서 문서 뷰 추가
- 인쇄 전용 standalone 레이아웃 (layouts/document.blade.php) 생성
- 한국 제조업 표준 견적서 양식 문서 뷰 생성 (A4 인쇄/PDF 최적화)
- RdController에 documentQuotation 메서드 추가
- /rd/ai-quotation/{id}/document 라우트 등록
- 상세 페이지에 "견적서 보기" 버튼 추가 (완료 상태만 표시)
- 한글 금액 변환, VAT 자동 계산, 비고란 포함
This commit is contained in:
@@ -54,6 +54,20 @@ public function createQuotation(Request $request): View|\Illuminate\Http\Respons
|
||||
return view('rd.ai-quotation.create');
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 견적 문서 (인쇄용 견적서)
|
||||
*/
|
||||
public function documentQuotation(int $id): View
|
||||
{
|
||||
$quotation = $this->quotationService->getById($id);
|
||||
|
||||
if (! $quotation || ! $quotation->isCompleted()) {
|
||||
abort(404, '완료된 견적만 문서로 조회할 수 있습니다.');
|
||||
}
|
||||
|
||||
return view('rd.ai-quotation.document', compact('quotation'));
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 견적 상세
|
||||
*/
|
||||
|
||||
41
resources/views/layouts/document.blade.php
Normal file
41
resources/views/layouts/document.blade.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>@yield('title', '문서') - {{ config('app.name') }}</title>
|
||||
@vite(['resources/css/app.css'])
|
||||
<style>
|
||||
@media print {
|
||||
body { margin: 0; padding: 0; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.no-print { display: none !important; }
|
||||
.document-page { box-shadow: none !important; margin: 0 !important; padding: 10mm 15mm !important; }
|
||||
}
|
||||
@page { size: A4; margin: 10mm 15mm; }
|
||||
</style>
|
||||
@stack('styles')
|
||||
</head>
|
||||
<body class="bg-gray-100 min-h-screen">
|
||||
{{-- 액션 버튼 (인쇄 시 숨김) --}}
|
||||
<div class="no-print fixed top-4 right-4 z-50 flex gap-2">
|
||||
<button onclick="window.print()"
|
||||
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/>
|
||||
</svg>
|
||||
<span class="text-sm font-medium">인쇄 / PDF</span>
|
||||
</button>
|
||||
<button onclick="window.close(); if(!window.closed) history.back();"
|
||||
class="bg-white hover:bg-gray-100 text-gray-700 px-4 py-2 rounded-lg shadow-lg border flex items-center gap-2 transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
</svg>
|
||||
<span class="text-sm font-medium">돌아가기</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@yield('content')
|
||||
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
235
resources/views/rd/ai-quotation/document.blade.php
Normal file
235
resources/views/rd/ai-quotation/document.blade.php
Normal file
@@ -0,0 +1,235 @@
|
||||
@extends('layouts.document')
|
||||
|
||||
@section('title', '견적서')
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.doc-table { width: 100%; border-collapse: collapse; }
|
||||
.doc-table th, .doc-table td { border: 1px solid #333; padding: 6px 10px; font-size: 13px; }
|
||||
.doc-table th { background-color: #f3f4f6; font-weight: 600; }
|
||||
.text-right { text-align: right; }
|
||||
.text-center { text-align: center; }
|
||||
.text-left { text-align: left; }
|
||||
.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; }
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@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;
|
||||
|
||||
// 한글 금액 변환
|
||||
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;
|
||||
}
|
||||
|
||||
$devTotalKorean = numberToKorean($devSubtotal);
|
||||
|
||||
// 필수 → 선택 순으로 정렬된 품목
|
||||
$sortedItems = $quotation->items->sortByDesc('is_required')->values();
|
||||
@endphp
|
||||
|
||||
<div class="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> 견적일로부터 30일</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p><span class="font-semibold">견적일자:</span> {{ $quotation->created_at->format('Y년 m월 d일') }}</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>{{ $quotation->title }}</td>
|
||||
<th>상 호</th>
|
||||
<td>(주)코드브릿지엑스</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>업 종</th>
|
||||
<td>{{ $company['industry'] ?? '-' }}</td>
|
||||
<th>대 표</th>
|
||||
<td>권형석</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>규 모</th>
|
||||
<td>{{ $company['scale'] ?? '-' }}</td>
|
||||
<th>주 소</th>
|
||||
<td>인천 남동구 남동대로 215번길 30</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>현재시스템</th>
|
||||
<td>{{ !empty($company['current_systems']) ? implode(', ', $company['current_systems']) : '-' }}</td>
|
||||
<th>연락처</th>
|
||||
<td>032-123-4567</td>
|
||||
</tr>
|
||||
</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">
|
||||
합계금액: 금 {{ $devTotalKorean }}원정
|
||||
<span class="text-base font-normal">(₩{{ number_format($devSubtotal) }})</span>
|
||||
</p>
|
||||
<p class="text-xs text-gray-600 mt-1">※ 부가가치세 별도 / 월 구독료 {{ number_format($monthlySubtotal) }}원 별도</p>
|
||||
</div>
|
||||
|
||||
{{-- 품목 테이블 --}}
|
||||
<table class="doc-table mb-6">
|
||||
<colgroup>
|
||||
<col style="width: 5%;">
|
||||
<col style="width: 7%;">
|
||||
<col style="width: 20%;">
|
||||
<col style="width: 33%;">
|
||||
<col style="width: 17.5%;">
|
||||
<col style="width: 17.5%;">
|
||||
</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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($sortedItems as $index => $item)
|
||||
<tr>
|
||||
<td class="text-center">{{ $index + 1 }}</td>
|
||||
<td class="text-center">
|
||||
{{ $item->is_required ? '필수' : '선택' }}
|
||||
</td>
|
||||
<td>{{ $item->module_name }}</td>
|
||||
<td class="text-xs">{{ Str::limit($item->reason, 80) }}</td>
|
||||
<td class="text-right">{{ number_format((int) $item->dev_cost) }}</td>
|
||||
<td class="text-right">{{ number_format((int) $item->monthly_fee) }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th colspan="4" class="text-right">소 계</th>
|
||||
<td class="text-right font-bold">{{ number_format($devSubtotal) }}</td>
|
||||
<td class="text-right font-bold">{{ number_format($monthlySubtotal) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="4" class="text-right">부가세 (10%)</th>
|
||||
<td class="text-right">{{ number_format($devVat) }}</td>
|
||||
<td class="text-right">{{ number_format($monthlyVat) }}</td>
|
||||
</tr>
|
||||
<tr style="background-color: #f3f4f6;">
|
||||
<th colspan="4" class="text-right text-base">합 계</th>
|
||||
<td class="text-right font-bold text-base">{{ number_format($devTotal) }}</td>
|
||||
<td class="text-right font-bold text-base">{{ number_format($monthlyTotal) }}</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>개발비 납부 조건: 계약 시 50%, 완료 시 50% 분할 납부</li>
|
||||
<li>월 구독료: 서비스 오픈일부터 과금 (월 {{ number_format($monthlyTotal) }}원, VAT 포함)</li>
|
||||
@if($estimatedMonths)
|
||||
<li>예상 구축 기간: {{ $estimatedMonths }}개월</li>
|
||||
@endif
|
||||
<li>본 견적서의 유효기간은 견적일로부터 30일입니다.</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>
|
||||
@endsection
|
||||
@@ -16,6 +16,12 @@
|
||||
<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())
|
||||
<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> 견적서 보기
|
||||
</a>
|
||||
@endif
|
||||
@if($quotation->isCompleted() || $quotation->status === 'failed')
|
||||
<button onclick="reanalyze()" id="reanalyzeBtn"
|
||||
class="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-lg transition">
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
use App\Http\Controllers\BoardController;
|
||||
use App\Http\Controllers\CategoryController;
|
||||
use App\Http\Controllers\CategorySyncController;
|
||||
use App\Http\Controllers\ClaudeCode\CoworkController as ClaudeCodeCoworkController;
|
||||
use App\Http\Controllers\ClaudeCode\NewsController as ClaudeCodeNewsController;
|
||||
use App\Http\Controllers\ClaudeCode\PricingController as ClaudeCodePricingController;
|
||||
use App\Http\Controllers\ClaudeCode\UsagePlanController as ClaudeCodeUsagePlanController;
|
||||
use App\Http\Controllers\CommonCodeController;
|
||||
use App\Http\Controllers\CommonCodeSyncController;
|
||||
use App\Http\Controllers\CustomerCenterController;
|
||||
@@ -27,19 +31,15 @@
|
||||
use App\Http\Controllers\ESign\EsignController;
|
||||
use App\Http\Controllers\ESign\EsignPublicController;
|
||||
use App\Http\Controllers\FcmController;
|
||||
use App\Http\Controllers\GoogleCloud\AiGuideController as GoogleCloudAiGuideController;
|
||||
use App\Http\Controllers\GoogleCloud\CloudApiPricingController as GoogleCloudCloudApiPricingController;
|
||||
use App\Http\Controllers\GoogleCloud\WorkspacePolicyController as GoogleCloudWorkspacePolicyController;
|
||||
use App\Http\Controllers\GoogleCloud\WorkspacePricingController as GoogleCloudWorkspacePricingController;
|
||||
use App\Http\Controllers\ItemFieldController;
|
||||
use App\Http\Controllers\ItemManagementController;
|
||||
use App\Http\Controllers\Juil\ConstructionSitePhotoController;
|
||||
use App\Http\Controllers\Juil\MeetingMinuteController;
|
||||
use App\Http\Controllers\Juil\PlanningController;
|
||||
use App\Http\Controllers\ClaudeCode\NewsController as ClaudeCodeNewsController;
|
||||
use App\Http\Controllers\ClaudeCode\CoworkController as ClaudeCodeCoworkController;
|
||||
use App\Http\Controllers\ClaudeCode\PricingController as ClaudeCodePricingController;
|
||||
use App\Http\Controllers\ClaudeCode\UsagePlanController as ClaudeCodeUsagePlanController;
|
||||
use App\Http\Controllers\GoogleCloud\WorkspacePolicyController as GoogleCloudWorkspacePolicyController;
|
||||
use App\Http\Controllers\GoogleCloud\WorkspacePricingController as GoogleCloudWorkspacePricingController;
|
||||
use App\Http\Controllers\GoogleCloud\AiGuideController as GoogleCloudAiGuideController;
|
||||
use App\Http\Controllers\GoogleCloud\CloudApiPricingController as GoogleCloudCloudApiPricingController;
|
||||
use App\Http\Controllers\Lab\StrategyController;
|
||||
use App\Http\Controllers\MenuController;
|
||||
use App\Http\Controllers\MenuSyncController;
|
||||
@@ -48,9 +48,9 @@
|
||||
use App\Http\Controllers\PostController;
|
||||
use App\Http\Controllers\ProfileController;
|
||||
use App\Http\Controllers\ProjectManagementController;
|
||||
use App\Http\Controllers\QuoteFormulaController;
|
||||
use App\Http\Controllers\RdController;
|
||||
use App\Http\Controllers\RoadmapController;
|
||||
use App\Http\Controllers\QuoteFormulaController;
|
||||
use App\Http\Controllers\RoleController;
|
||||
use App\Http\Controllers\RolePermissionController;
|
||||
use App\Http\Controllers\Sales\SalesProductController;
|
||||
@@ -375,6 +375,7 @@
|
||||
Route::get('/', [RdController::class, 'index'])->name('index');
|
||||
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}', [RdController::class, 'showQuotation'])->name('ai-quotation.show');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user