Files
sam-api/app/Services/Quote/QuoteNumberService.php
hskwon 40ca8b8697 feat: [quote] 견적 API Phase 2-3 완료 (Service + Controller Layer)
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.* 성공 메시지
2025-12-04 22:03:40 +09:00

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();
}
}