feat(quote): quote_type 컬럼 추가 및 건설 견적 필터링 구현

- quotes 테이블에 quote_type 컬럼 추가 (manufacturing/construction)
- Quote 모델에 TYPE 상수 및 스코프 메서드 추가
- QuoteService.index()에 quote_type 필터 적용
- QuoteService.upsertFromSiteBriefing()에 construction 타입 설정
- QuoteIndexRequest에 quote_type 유효성 검증 추가
- 기존 site_briefing_id 있는 데이터는 construction으로 자동 업데이트

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-01-13 09:55:22 +09:00
parent 3a40db9444
commit 84ad9e1fc4
4 changed files with 218 additions and 0 deletions

View File

@@ -7,6 +7,7 @@
use App\Models\Quote\Quote;
use App\Models\Quote\QuoteItem;
use App\Models\Quote\QuoteRevision;
use App\Models\Tenants\SiteBriefing;
use App\Services\Service;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
@@ -30,6 +31,7 @@ public function index(array $params): LengthAwarePaginator
$page = (int) ($params['page'] ?? 1);
$size = (int) ($params['size'] ?? 20);
$q = trim((string) ($params['q'] ?? ''));
$quoteType = $params['quote_type'] ?? null;
$status = $params['status'] ?? null;
$productCategory = $params['product_category'] ?? null;
$clientId = $params['client_id'] ?? null;
@@ -37,14 +39,25 @@ public function index(array $params): LengthAwarePaginator
$dateTo = $params['date_to'] ?? null;
$sortBy = $params['sort_by'] ?? 'registration_date';
$sortOrder = $params['sort_order'] ?? 'desc';
$withItems = filter_var($params['with_items'] ?? false, FILTER_VALIDATE_BOOLEAN);
$query = Quote::query()->where('tenant_id', $tenantId);
// items 포함 (수주 전환용)
if ($withItems) {
$query->with(['items', 'client:id,name,contact_person,phone']);
}
// 검색어
if ($q !== '') {
$query->search($q);
}
// 견적 유형 필터
if ($quoteType) {
$query->where('quote_type', $quoteType);
}
// 상태 필터
if ($status) {
$query->where('status', $status);
@@ -585,4 +598,95 @@ private function createRevision(Quote $quote, int $userId): QuoteRevision
'previous_data' => $previousData,
]);
}
/**
* 현장설명회에서 견적 Upsert (생성 또는 업데이트)
*
* 참석완료 상태일 때 견적을 자동 생성하거나 업데이트합니다.
* - 견적이 없으면: 견적대기(pending) 상태로 신규 생성
* - 견적이 있으면: 현장설명회 정보로 업데이트 (거래처, 현장 등)
*/
public function upsertFromSiteBriefing(SiteBriefing $siteBriefing): Quote
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
// 기존 견적 조회
$existingQuote = Quote::where('tenant_id', $tenantId)
->where('site_briefing_id', $siteBriefing->id)
->first();
return DB::transaction(function () use ($siteBriefing, $tenantId, $userId, $existingQuote) {
if ($existingQuote) {
// 기존 견적 업데이트 (현장설명회 정보 동기화)
$existingQuote->update([
// 발주처 정보 동기화
'client_id' => $siteBriefing->partner_id,
'client_name' => $siteBriefing->partner?->name,
// 현장 정보 동기화
'site_id' => $siteBriefing->site_id,
'site_name' => $siteBriefing->title,
// 감사
'updated_by' => $userId,
]);
return $existingQuote->refresh();
}
// 신규 견적 생성
$quoteNumber = $this->numberService->generate('SCREEN');
return Quote::create([
'tenant_id' => $tenantId,
'quote_type' => Quote::TYPE_CONSTRUCTION,
'site_briefing_id' => $siteBriefing->id,
'quote_number' => $quoteNumber,
'registration_date' => now()->toDateString(),
// 발주처 정보 (현장설명회에서 복사)
'client_id' => $siteBriefing->partner_id,
'client_name' => $siteBriefing->partner?->name,
// 현장 정보 (현장설명회에서 복사)
'site_id' => $siteBriefing->site_id,
'site_name' => $siteBriefing->title,
// 제품 카테고리 없음 (pending 상태이므로)
'product_category' => null,
// 금액 정보 (빈 값)
'material_cost' => 0,
'labor_cost' => 0,
'install_cost' => 0,
'subtotal' => 0,
'discount_rate' => 0,
'discount_amount' => 0,
'total_amount' => 0,
// 상태 관리 (견적대기)
'status' => Quote::STATUS_PENDING,
'current_revision' => 0,
'is_final' => false,
// 비고 (현장설명회 정보 기록)
'remarks' => "현장설명회 참석완료로 자동생성 (현설번호: {$siteBriefing->briefing_code})",
// 감사
'created_by' => $userId,
]);
});
}
/**
* @deprecated Use upsertFromSiteBriefing() instead
*/
public function createFromSiteBriefing(SiteBriefing $siteBriefing): ?Quote
{
return $this->upsertFromSiteBriefing($siteBriefing);
}
/**
* 현장설명회 ID로 연결된 견적 조회
*/
public function findBySiteBriefingId(int $siteBriefingId): ?Quote
{
$tenantId = $this->tenantId();
return Quote::where('tenant_id', $tenantId)
->where('site_briefing_id', $siteBriefingId)
->first();
}
}