tenantId(); $quote = Quote::with(['items', 'client']) ->where('tenant_id', $tenantId) ->find($quoteId); if (! $quote) { throw new NotFoundHttpException(__('error.quote_not_found')); } // PDF 생성 데이터 준비 $data = $this->preparePdfData($quote); // TODO: 실제 PDF 생성 로직 구현 (barryvdh/laravel-dompdf 등 사용) // 현재는 기본 구조만 제공 $filename = $this->generateFilename($quote); $path = "quotes/{$tenantId}/{$filename}"; // 임시: PDF 생성 시뮬레이션 // Storage::disk('local')->put($path, $this->generatePdfContent($data)); return [ 'quote_id' => $quoteId, 'quote_number' => $quote->quote_number, 'filename' => $filename, 'path' => $path, 'generated_at' => now()->toDateTimeString(), 'data' => $data, // 개발 단계에서 확인용 ]; } /** * 견적서 이메일 발송 */ public function sendEmail(int $quoteId, array $options = []): array { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $quote = Quote::with(['items', 'client']) ->where('tenant_id', $tenantId) ->find($quoteId); if (! $quote) { throw new NotFoundHttpException(__('error.quote_not_found')); } // 수신자 정보 $recipientEmail = $options['email'] ?? $quote->client?->email ?? null; $recipientName = $options['name'] ?? $quote->client_name ?? $quote->client?->name ?? null; if (! $recipientEmail) { throw new BadRequestHttpException(__('error.quote_email_not_found')); } // 이메일 옵션 $subject = $options['subject'] ?? $this->getDefaultEmailSubject($quote); $message = $options['message'] ?? $this->getDefaultEmailMessage($quote); $cc = $options['cc'] ?? []; $attachPdf = $options['attach_pdf'] ?? true; // PDF 생성 (첨부 시) $pdfInfo = null; if ($attachPdf) { $pdfInfo = $this->generatePdf($quoteId); } // TODO: 실제 이메일 발송 로직 구현 (Laravel Mail 사용) // Mail::to($recipientEmail)->send(new QuoteMail($quote, $pdfInfo)); // 발송 이력 기록 $sendLog = $this->createSendLog($quote, [ 'channel' => self::CHANNEL_EMAIL, 'recipient_email' => $recipientEmail, 'recipient_name' => $recipientName, 'subject' => $subject, 'message' => $message, 'cc' => $cc, 'pdf_path' => $pdfInfo['path'] ?? null, 'sent_by' => $userId, ]); // 견적 상태 업데이트 (draft → sent) if ($quote->status === Quote::STATUS_DRAFT) { $quote->update([ 'status' => Quote::STATUS_SENT, 'updated_by' => $userId, ]); } return [ 'success' => true, 'message' => __('message.quote_email_sent'), 'send_log' => $sendLog, 'quote_status' => $quote->fresh()->status, ]; } /** * 견적서 카카오톡 발송 */ public function sendKakao(int $quoteId, array $options = []): array { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $quote = Quote::with(['items', 'client']) ->where('tenant_id', $tenantId) ->find($quoteId); if (! $quote) { throw new NotFoundHttpException(__('error.quote_not_found')); } // 수신자 정보 $recipientPhone = $options['phone'] ?? $quote->contact ?? $quote->client?->phone ?? null; $recipientName = $options['name'] ?? $quote->client_name ?? $quote->client?->name ?? null; if (! $recipientPhone) { throw new BadRequestHttpException(__('error.quote_phone_not_found')); } // 알림톡 템플릿 데이터 $templateCode = $options['template_code'] ?? 'QUOTE_SEND'; $templateData = $this->prepareKakaoTemplateData($quote, $options); // TODO: 실제 카카오 알림톡 발송 로직 구현 // 외부 API 연동 필요 (NHN Cloud, Solapi 등) // 발송 이력 기록 $sendLog = $this->createSendLog($quote, [ 'channel' => self::CHANNEL_KAKAO, 'recipient_phone' => $recipientPhone, 'recipient_name' => $recipientName, 'template_code' => $templateCode, 'template_data' => $templateData, 'sent_by' => $userId, ]); // 견적 상태 업데이트 (draft → sent) if ($quote->status === Quote::STATUS_DRAFT) { $quote->update([ 'status' => Quote::STATUS_SENT, 'updated_by' => $userId, ]); } return [ 'success' => true, 'message' => __('message.quote_kakao_sent'), 'send_log' => $sendLog, 'quote_status' => $quote->fresh()->status, ]; } /** * 발송 이력 조회 */ public function getSendHistory(int $quoteId): array { $tenantId = $this->tenantId(); $quote = Quote::where('tenant_id', $tenantId)->find($quoteId); if (! $quote) { throw new NotFoundHttpException(__('error.quote_not_found')); } // TODO: 발송 이력 테이블 조회 // QuoteSendLog::where('quote_id', $quoteId)->orderByDesc('created_at')->get(); return [ 'quote_id' => $quoteId, 'quote_number' => $quote->quote_number, 'history' => [], // 발송 이력 배열 ]; } /** * PDF 데이터 준비 */ private function preparePdfData(Quote $quote): array { // 회사 정보 (테넌트 설정에서) $companyInfo = [ 'name' => config('app.company_name', 'KD 건축자재'), 'address' => config('app.company_address', ''), 'phone' => config('app.company_phone', ''), 'email' => config('app.company_email', ''), 'registration_number' => config('app.company_registration_number', ''), ]; // 거래처 정보 $clientInfo = [ 'name' => $quote->client_name ?? $quote->client?->name ?? '', 'manager' => $quote->manager ?? '', 'contact' => $quote->contact ?? '', 'address' => $quote->client?->address ?? '', ]; // 견적 기본 정보 $quoteInfo = [ 'quote_number' => $quote->quote_number, 'registration_date' => $quote->registration_date?->format('Y-m-d'), 'completion_date' => $quote->completion_date?->format('Y-m-d'), 'product_category' => $quote->product_category, 'product_name' => $quote->product_name, 'site_name' => $quote->site_name, 'author' => $quote->author, ]; // 규격 정보 $specInfo = [ 'open_size_width' => $quote->open_size_width, 'open_size_height' => $quote->open_size_height, 'quantity' => $quote->quantity, 'unit_symbol' => $quote->unit_symbol, 'floors' => $quote->floors, ]; // 품목 목록 $items = $quote->items->map(fn ($item) => [ 'item_code' => $item->item_code, 'item_name' => $item->item_name, 'specification' => $item->specification, 'unit' => $item->unit, 'quantity' => $item->calculated_quantity, 'unit_price' => $item->unit_price, 'total_price' => $item->total_price, 'note' => $item->note, ])->toArray(); // 금액 정보 $amounts = [ 'material_cost' => $quote->material_cost, 'labor_cost' => $quote->labor_cost, 'install_cost' => $quote->install_cost, 'subtotal' => $quote->subtotal, 'discount_rate' => $quote->discount_rate, 'discount_amount' => $quote->discount_amount, 'total_amount' => $quote->total_amount, ]; return [ 'company' => $companyInfo, 'client' => $clientInfo, 'quote' => $quoteInfo, 'spec' => $specInfo, 'items' => $items, 'amounts' => $amounts, 'remarks' => $quote->remarks, 'notes' => $quote->notes, ]; } /** * 파일명 생성 */ private function generateFilename(Quote $quote): string { $date = now()->format('Ymd'); return "quote_{$quote->quote_number}_{$date}.pdf"; } /** * 기본 이메일 제목 */ private function getDefaultEmailSubject(Quote $quote): string { return "[견적서] {$quote->quote_number} - {$quote->product_name}"; } /** * 기본 이메일 본문 */ private function getDefaultEmailMessage(Quote $quote): string { $clientName = $quote->client_name ?? '고객'; return <<quote_number} - 제품명: {$quote->product_name} - 현장명: {$quote->site_name} - 견적금액: ₩ {$quote->total_amount} 첨부된 견적서를 확인해 주시기 바랍니다. 문의사항이 있으시면 언제든 연락 주세요. 감사합니다. MESSAGE; } /** * 카카오 알림톡 템플릿 데이터 준비 */ private function prepareKakaoTemplateData(Quote $quote, array $options = []): array { return [ 'quote_number' => $quote->quote_number, 'client_name' => $quote->client_name ?? '', 'product_name' => $quote->product_name ?? '', 'site_name' => $quote->site_name ?? '', 'total_amount' => number_format((float) $quote->total_amount), 'registration_date' => $quote->registration_date?->format('Y-m-d'), 'view_url' => $options['view_url'] ?? '', // 견적서 조회 URL ]; } /** * 발송 이력 생성 */ private function createSendLog(Quote $quote, array $data): array { // TODO: QuoteSendLog 모델 생성 후 실제 DB 저장 구현 // QuoteSendLog::create([ // 'tenant_id' => $quote->tenant_id, // 'quote_id' => $quote->id, // 'channel' => $data['channel'], // ... // ]); return array_merge([ 'quote_id' => $quote->id, 'quote_number' => $quote->quote_number, 'status' => self::STATUS_PENDING, 'sent_at' => now()->toDateTimeString(), ], $data); } }