Files
sam-manage/resources/views/rd/ai-quotation/create.blade.php
김보곤 25795f8612 feat: [ai-quotation] 제조 견적서 자동 생성 기능 추가
- AI 2단계 분석: 고객 인터뷰 → 요구사항 추출 → 견적 산출
- 모델 확장: AiQuotation(모드/견적번호), AiQuotationItem(규격/단가/금액)
- AiQuotePriceTable 모델 신규 생성
- Create 페이지: 모듈/제조 모드 탭, 제품 카테고리, 고객 정보 입력
- Show 페이지: 제조 모드 분기 렌더링 (품목/금액/고객정보)
- Edit 페이지: 품목 인라인 편집, 할인/부가세/조건 입력
- Document: 한국 표준 제조업 견적서 양식 템플릿
- Controller/Route: update 엔드포인트, edit 라우트 추가
2026-03-03 15:58:16 +09:00

332 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@extends('layouts.app')
@section('title', 'AI 견적서 생성')
@section('content')
<!-- 페이지 헤더 -->
<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-robot-line text-purple-600"></i>
AI 견적서 생성
</h1>
<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>
</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">인터뷰 내용 입력</h2>
<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>
<input type="text" name="title" id="inputTitle" required maxlength="200"
placeholder="예: (주)대한기계 ERP 도입 견적"
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500">
</div>
<!-- 입력 유형 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">입력 유형</label>
<div class="flex gap-3">
<label 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">
<input type="radio" name="input_type" value="text" checked class="text-purple-600">
<i class="ri-file-text-line"></i> 텍스트 입력
</label>
<label class="flex items-center gap-2 px-4 py-2.5 border border-gray-300 bg-gray-50 text-gray-400 rounded-lg cursor-not-allowed" title="Phase 2 예정">
<input type="radio" name="input_type" value="voice" disabled>
<i class="ri-mic-line"></i> 음성 파일 (Phase 2)
</label>
</div>
</div>
<!-- AI Provider -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">AI Provider</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="providerGemini">
<input type="radio" name="ai_provider" value="gemini" checked class="text-blue-600"
onchange="updateProviderUI()">
<span class="font-medium">Gemini</span>
<span class="text-xs text-gray-400">(기본)</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="providerClaude">
<input type="radio" name="ai_provider" value="claude" class="text-orange-600"
onchange="updateProviderUI()">
<span class="font-medium">Claude</span>
</label>
</div>
</div>
<!-- 인터뷰 내용 -->
<div>
<div class="flex items-center gap-2 mb-1">
<label class="text-sm font-medium text-gray-700">인터뷰 내용 <span class="text-red-500">*</span></label>
<button type="button" onclick="fillSampleInterview()" title="샘플 인터뷰 자동 입력"
class="w-7 h-7 flex items-center justify-center rounded-full bg-amber-100 text-amber-600 hover:bg-amber-200 hover:text-amber-700 transition">
<i class="ri-flashlight-line text-sm"></i>
</button>
</div>
<textarea name="input_text" id="inputText" rows="12" required
placeholder="고객사 직원과의 인터뷰 내용을 입력하세요."
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 resize-y"></textarea>
<p class="text-xs text-gray-400 mt-1">인터뷰 내용이 구체적일수록 정확한 견적이 생성됩니다.</p>
</div>
<!-- 제출 -->
<div class="flex justify-end gap-3 pt-4 border-t border-gray-100">
<a href="{{ route('rd.ai-quotation.index') }}" 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="submitBtn"
class="px-6 py-2.5 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition flex items-center gap-2">
<i class="ri-robot-line"></i> AI 분석 실행
</button>
</div>
</form>
</div>
<!-- 결과 영역 (숨김) -->
<div id="resultArea" class="mt-6 hidden">
<div id="resultContent"></div>
</div>
@endsection
@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');
const selected = document.querySelector('input[name="ai_provider"]:checked').value;
gemini.classList.toggle('border-blue-500', selected === 'gemini');
gemini.classList.toggle('bg-blue-50', selected === 'gemini');
gemini.classList.toggle('border-gray-300', selected !== 'gemini');
claude.classList.toggle('border-orange-500', selected === 'claude');
claude.classList.toggle('bg-orange-50', selected === 'claude');
claude.classList.toggle('border-gray-300', selected !== 'claude');
}
document.getElementById('quotationForm').addEventListener('submit', async function(e) {
e.preventDefault();
const btn = document.getElementById('submitBtn');
const originalHtml = btn.innerHTML;
// 유효성 검사
const title = document.getElementById('inputTitle').value.trim();
const text = document.getElementById('inputText').value.trim();
if (!title || !text) {
alert('제목과 인터뷰 내용을 모두 입력하세요.');
return;
}
// 로딩 상태
btn.disabled = true;
btn.innerHTML = '<i class="ri-loader-4-line animate-spin"></i> AI 분석중... (30초~1분 소요)';
btn.classList.add('opacity-75');
try {
const formData = new FormData(this);
const data = Object.fromEntries(formData.entries());
const token = document.querySelector('meta[name="api-token"]')?.content
|| sessionStorage.getItem('api_token') || '';
const response = await fetch('{{ url("/api/admin/rd/ai-quotation") }}', {
method: 'POST',
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(data),
});
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}`;
}
}
} catch (err) {
console.error('AI 분석 요청 실패:', err);
alert('서버 통신 중 오류가 발생했습니다.');
} finally {
btn.disabled = false;
btn.innerHTML = originalHtml;
btn.classList.remove('opacity-75');
}
});
function fillSampleInterview() {
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: 현재 업무 관리는 어떻게 하고 계세요?
A: 솔직히 거의 다 엑셀이에요. 직원 인사정보도 엑셀 파일로 관리하고, 출퇴근은 지문인식기가 있긴 한데 데이터를 엑셀로 뽑아서 월말에 수작업으로 정리해요. 급여도 엑셀로 계산하는데 4대보험 계산할 때마다 실수가 나서 스트레스예요.
Q: 영업 쪽은 어떤가요?
A: 영업팀이 4명인데, 견적서를 한글 프로그램으로 만들어서 이메일로 보내고 있어요. 고객한테 보낸 견적 이력이 각자 컴퓨터에 흩어져 있어서, 누가 퇴사하면 그 고객 정보가 다 날아가요. 수주 현황도 화이트보드에 적어놓고 회의 때 공유하는 수준이에요. CRM 같은 건 꿈도 못 꿔요.
Q: 생산 현장은요?
A: 작업일보를 종이에 써서 제출해요. 작업지시서도 종이로 출력해서 현장에 붙여놓고요. 불량이 나면 어디서 발생했는지 추적이 잘 안 돼요. LOT 관리를 하고 싶은데 현재는 수기로 하다 보니 한계가 있어요. 납기 관리도 엑셀인데, 긴급 주문 들어오면 일정 조정하느라 난리예요.
Q: 가장 시급하게 개선하고 싶은 부분은?
A: 일단 생산 현황을 실시간으로 볼 수 있었으면 좋겠고, 재고 관리가 급해요. 그리고 직원 관리랑 급여 계산을 자동화하고 싶어요. 영업 쪽도 견적서 관리가 너무 안 되니까 개선이 필요해요.`;
}
}
// 초기 UI
updateProviderUI();
updateModeUI();
</script>
@endpush