Phase 2 - Service Layer:
- QuoteService: 견적 CRUD + 상태관리 (확정/전환)
- QuoteNumberService: 견적번호 채번 (KD-{PREFIX}-YYMMDD-SEQ)
- FormulaEvaluatorService: 수식 평가 엔진 (SUM, IF, ROUND 등)
- QuoteCalculationService: 자동산출 (스크린/철재 제품)
- QuoteDocumentService: PDF 생성 및 이메일/카카오 발송
Phase 3 - Controller Layer:
- QuoteController: 16개 엔드포인트
- FormRequest 7개: Index, Store, Update, BulkDelete, Calculate, SendEmail, SendKakao
- QuoteApi.php: Swagger 문서 (12개 스키마, 16개 엔드포인트)
- routes/api.php: 16개 라우트 등록
i18n 키 추가:
- error.php: quote_not_found, formula_* 등
- message.php: quote.* 성공 메시지
116 lines
3.0 KiB
PHP
116 lines
3.0 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Quote;
|
|
|
|
use App\Models\Quote\Quote;
|
|
use App\Services\Service;
|
|
|
|
class QuoteNumberService extends Service
|
|
{
|
|
/**
|
|
* 견적번호 생성
|
|
*
|
|
* 형식: KD-{PREFIX}-{YYMMDD}-{SEQ}
|
|
* 예시: KD-SC-251204-01 (스크린), KD-ST-251204-01 (철재)
|
|
*/
|
|
public function generate(?string $productCategory = null): string
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
|
|
// 제품 카테고리에 따른 접두어
|
|
$prefix = match ($productCategory) {
|
|
Quote::CATEGORY_SCREEN => 'SC',
|
|
Quote::CATEGORY_STEEL => 'ST',
|
|
default => 'SC',
|
|
};
|
|
|
|
// 날짜 부분 (YYMMDD)
|
|
$dateStr = now()->format('ymd');
|
|
|
|
// 오늘 날짜 기준으로 마지막 견적번호 조회
|
|
$pattern = "KD-{$prefix}-{$dateStr}-%";
|
|
|
|
$lastQuote = Quote::withTrashed()
|
|
->where('tenant_id', $tenantId)
|
|
->where('quote_number', 'like', $pattern)
|
|
->orderBy('quote_number', 'desc')
|
|
->first();
|
|
|
|
// 순번 계산
|
|
$sequence = 1;
|
|
if ($lastQuote) {
|
|
// KD-SC-251204-01 에서 마지막 숫자 추출
|
|
$parts = explode('-', $lastQuote->quote_number);
|
|
if (count($parts) >= 4) {
|
|
$lastSeq = (int) end($parts);
|
|
$sequence = $lastSeq + 1;
|
|
}
|
|
}
|
|
|
|
// 2자리 순번 (01, 02, ...)
|
|
$seqStr = str_pad((string) $sequence, 2, '0', STR_PAD_LEFT);
|
|
|
|
return "KD-{$prefix}-{$dateStr}-{$seqStr}";
|
|
}
|
|
|
|
/**
|
|
* 견적번호 미리보기
|
|
*/
|
|
public function preview(?string $productCategory = null): array
|
|
{
|
|
$quoteNumber = $this->generate($productCategory);
|
|
|
|
return [
|
|
'quote_number' => $quoteNumber,
|
|
'product_category' => $productCategory ?? Quote::CATEGORY_SCREEN,
|
|
'generated_at' => now()->toDateTimeString(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 견적번호 형식 검증
|
|
*/
|
|
public function validate(string $quoteNumber): bool
|
|
{
|
|
// 형식: KD-XX-YYMMDD-NN
|
|
return (bool) preg_match('/^KD-[A-Z]{2}-\d{6}-\d{2,}$/', $quoteNumber);
|
|
}
|
|
|
|
/**
|
|
* 견적번호 파싱
|
|
*/
|
|
public function parse(string $quoteNumber): ?array
|
|
{
|
|
if (! $this->validate($quoteNumber)) {
|
|
return null;
|
|
}
|
|
|
|
$parts = explode('-', $quoteNumber);
|
|
|
|
return [
|
|
'prefix' => $parts[0], // KD
|
|
'category_code' => $parts[1], // SC or ST
|
|
'date' => $parts[2], // YYMMDD
|
|
'sequence' => (int) $parts[3], // 순번
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 견적번호 중복 체크
|
|
*/
|
|
public function isUnique(string $quoteNumber, ?int $excludeId = null): bool
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
|
|
$query = Quote::withTrashed()
|
|
->where('tenant_id', $tenantId)
|
|
->where('quote_number', $quoteNumber);
|
|
|
|
if ($excludeId) {
|
|
$query->where('id', '!=', $excludeId);
|
|
}
|
|
|
|
return ! $query->exists();
|
|
}
|
|
}
|