2026-01-29 19:30:46 +09:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Services\Quote;
|
|
|
|
|
|
|
|
|
|
use App\Models\Items\Item;
|
|
|
|
|
use App\Models\Products\Price;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 견적 단가 조회 서비스
|
|
|
|
|
*
|
|
|
|
|
* items + item_details + prices 테이블 기반 단가 조회
|
|
|
|
|
* kd_price_tables 대체
|
|
|
|
|
*/
|
|
|
|
|
class EstimatePriceService
|
|
|
|
|
{
|
|
|
|
|
private int $tenantId;
|
|
|
|
|
|
|
|
|
|
/** @var array 단가 캐시 (세션 내 중복 조회 방지) */
|
|
|
|
|
private array $cache = [];
|
|
|
|
|
|
|
|
|
|
public function __construct(int $tenantId)
|
|
|
|
|
{
|
|
|
|
|
$this->tenantId = $tenantId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
// BDmodels 단가 조회 (절곡품)
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* BDmodels 단가 조회
|
|
|
|
|
*
|
|
|
|
|
* item_details 컬럼 매핑:
|
|
|
|
|
* product_category = 'bdmodels'
|
|
|
|
|
* part_type = seconditem (가이드레일, 케이스, 마구리, L-BAR, 하단마감재, 보강평철, 연기차단재)
|
|
|
|
|
* specification = spec (120*70, 500*380 등)
|
|
|
|
|
* items.attributes:
|
|
|
|
|
* model_name = KSS01, KSS02 등
|
|
|
|
|
* finishing_type = SUS, EGI
|
|
|
|
|
*/
|
|
|
|
|
public function getBDModelPrice(
|
|
|
|
|
string $secondItem,
|
|
|
|
|
?string $modelName = null,
|
|
|
|
|
?string $finishingType = null,
|
|
|
|
|
?string $spec = null
|
|
|
|
|
): float {
|
|
|
|
|
$cacheKey = "bdmodel:{$secondItem}:{$modelName}:{$finishingType}:{$spec}";
|
|
|
|
|
|
|
|
|
|
if (isset($this->cache[$cacheKey])) {
|
|
|
|
|
return $this->cache[$cacheKey];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$query = DB::table('items')
|
|
|
|
|
->join('item_details', 'item_details.item_id', '=', 'items.id')
|
|
|
|
|
->join('prices', 'prices.item_id', '=', 'items.id')
|
|
|
|
|
->where('items.tenant_id', $this->tenantId)
|
|
|
|
|
->where('items.is_active', true)
|
|
|
|
|
->whereNull('items.deleted_at')
|
|
|
|
|
->where('item_details.product_category', 'bdmodels')
|
|
|
|
|
->where('item_details.part_type', $secondItem);
|
|
|
|
|
|
|
|
|
|
if ($modelName) {
|
|
|
|
|
$query->where('items.attributes->model_name', $modelName);
|
|
|
|
|
} else {
|
|
|
|
|
$query->where(function ($q) {
|
|
|
|
|
$q->whereNull('items.attributes->model_name')
|
|
|
|
|
->orWhere('items.attributes->model_name', '');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($finishingType) {
|
|
|
|
|
$query->where('items.attributes->finishing_type', $finishingType);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($spec) {
|
|
|
|
|
$query->where('item_details.specification', $spec);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 현재 유효한 단가
|
|
|
|
|
$today = now()->toDateString();
|
|
|
|
|
$query->where('prices.effective_from', '<=', $today)
|
|
|
|
|
->where(function ($q) use ($today) {
|
|
|
|
|
$q->whereNull('prices.effective_to')
|
|
|
|
|
->orWhere('prices.effective_to', '>=', $today);
|
|
|
|
|
})
|
|
|
|
|
->whereNull('prices.deleted_at');
|
|
|
|
|
|
|
|
|
|
$price = (float) ($query->value('prices.sales_price') ?? 0);
|
|
|
|
|
|
|
|
|
|
$this->cache[$cacheKey] = $price;
|
|
|
|
|
|
|
|
|
|
return $price;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 케이스 단가
|
|
|
|
|
*/
|
|
|
|
|
public function getCasePrice(string $spec): float
|
|
|
|
|
{
|
|
|
|
|
return $this->getBDModelPrice('케이스', null, null, $spec);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 가이드레일 단가
|
|
|
|
|
*/
|
|
|
|
|
public function getGuideRailPrice(string $modelName, string $finishingType, string $spec): float
|
|
|
|
|
{
|
|
|
|
|
return $this->getBDModelPrice('가이드레일', $modelName, $finishingType, $spec);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 하단마감재(하장바) 단가
|
|
|
|
|
*/
|
|
|
|
|
public function getBottomBarPrice(string $modelName, string $finishingType): float
|
|
|
|
|
{
|
|
|
|
|
return $this->getBDModelPrice('하단마감재', $modelName, $finishingType);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* L-BAR 단가
|
|
|
|
|
*/
|
|
|
|
|
public function getLBarPrice(string $modelName): float
|
|
|
|
|
{
|
|
|
|
|
return $this->getBDModelPrice('L-BAR', $modelName);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 보강평철 단가
|
|
|
|
|
*/
|
|
|
|
|
public function getFlatBarPrice(): float
|
|
|
|
|
{
|
|
|
|
|
return $this->getBDModelPrice('보강평철');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 케이스 마구리 단가
|
|
|
|
|
*/
|
|
|
|
|
public function getCaseCapPrice(string $spec): float
|
|
|
|
|
{
|
|
|
|
|
return $this->getBDModelPrice('마구리', null, null, $spec);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 케이스용 연기차단재 단가
|
|
|
|
|
*/
|
|
|
|
|
public function getCaseSmokeBlockPrice(): float
|
|
|
|
|
{
|
|
|
|
|
return $this->getBDModelPrice('케이스용 연기차단재');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 가이드레일용 연기차단재 단가
|
|
|
|
|
*/
|
|
|
|
|
public function getRailSmokeBlockPrice(): float
|
|
|
|
|
{
|
|
|
|
|
return $this->getBDModelPrice('가이드레일용 연기차단재');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
// 모터/제어기 단가
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 모터 단가
|
2026-01-29 22:00:15 +09:00
|
|
|
*
|
|
|
|
|
* chandj col2는 '150K(S)', '300K(S)', '300K' 등 다양한 형식
|
|
|
|
|
* handler는 '150K', '300K' 등 단순 용량으로 호출
|
|
|
|
|
* LIKE 매칭 + 380V 기본 전압 필터 적용
|
2026-01-29 19:30:46 +09:00
|
|
|
*/
|
2026-01-29 22:00:15 +09:00
|
|
|
public function getMotorPrice(string $motorCapacity, string $voltage = '380'): float
|
2026-01-29 19:30:46 +09:00
|
|
|
{
|
2026-01-29 22:00:15 +09:00
|
|
|
$cacheKey = "motor:{$motorCapacity}:{$voltage}";
|
|
|
|
|
|
|
|
|
|
if (isset($this->cache[$cacheKey])) {
|
|
|
|
|
return $this->cache[$cacheKey];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$today = now()->toDateString();
|
|
|
|
|
|
|
|
|
|
$price = (float) (DB::table('items')
|
|
|
|
|
->join('item_details', 'item_details.item_id', '=', 'items.id')
|
|
|
|
|
->join('prices', 'prices.item_id', '=', 'items.id')
|
|
|
|
|
->where('items.tenant_id', $this->tenantId)
|
|
|
|
|
->where('items.is_active', true)
|
|
|
|
|
->whereNull('items.deleted_at')
|
|
|
|
|
->where('item_details.product_category', 'motor')
|
|
|
|
|
->where('item_details.part_type', 'LIKE', "{$motorCapacity}%")
|
|
|
|
|
->where('items.attributes->voltage', $voltage)
|
|
|
|
|
->where('prices.effective_from', '<=', $today)
|
|
|
|
|
->where(function ($q) use ($today) {
|
|
|
|
|
$q->whereNull('prices.effective_to')
|
|
|
|
|
->orWhere('prices.effective_to', '>=', $today);
|
|
|
|
|
})
|
|
|
|
|
->whereNull('prices.deleted_at')
|
|
|
|
|
->value('prices.sales_price') ?? 0);
|
|
|
|
|
|
|
|
|
|
$this->cache[$cacheKey] = $price;
|
|
|
|
|
|
|
|
|
|
return $price;
|
2026-01-29 19:30:46 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 제어기 단가
|
|
|
|
|
*/
|
|
|
|
|
public function getControllerPrice(string $controllerType): float
|
|
|
|
|
{
|
2026-01-29 22:00:15 +09:00
|
|
|
return $this->getEstimatePartPrice('controller', $controllerType);
|
2026-01-29 19:30:46 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
// 부자재 단가
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 샤프트 단가
|
2026-01-29 22:00:15 +09:00
|
|
|
*
|
|
|
|
|
* chandj col10은 '0.3', '3', '6' 등 혼재 포맷
|
|
|
|
|
* 정수면 '6', 소수면 '0.3' 그대로 저장됨
|
2026-01-29 19:30:46 +09:00
|
|
|
*/
|
|
|
|
|
public function getShaftPrice(string $size, float $length): float
|
|
|
|
|
{
|
2026-01-29 22:00:15 +09:00
|
|
|
// chandj 원본 포맷에 맞게 변환: 정수면 정수형, 소수면 소수형
|
|
|
|
|
$lengthStr = ($length == (int) $length) ? (string) (int) $length : (string) $length;
|
2026-01-29 19:30:46 +09:00
|
|
|
$cacheKey = "shaft:{$size}:{$lengthStr}";
|
|
|
|
|
|
|
|
|
|
if (isset($this->cache[$cacheKey])) {
|
|
|
|
|
return $this->cache[$cacheKey];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$price = $this->getEstimatePartPriceBySpec('shaft', $size, $lengthStr);
|
|
|
|
|
$this->cache[$cacheKey] = $price;
|
|
|
|
|
|
|
|
|
|
return $price;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 파이프 단가
|
|
|
|
|
*/
|
|
|
|
|
public function getPipePrice(string $thickness, int $length): float
|
|
|
|
|
{
|
|
|
|
|
return $this->getEstimatePartPriceBySpec('pipe', $thickness, (string) $length);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-29 22:00:15 +09:00
|
|
|
* 모터 받침용 앵글 단가 (bracket angle)
|
|
|
|
|
*
|
|
|
|
|
* 5130: calculateAngle(qty, itemList, '스크린용') → col2 검색
|
|
|
|
|
* chandj col2 값: '스크린용', '철제300K', '철제400K', '철제800K'
|
2026-01-29 19:30:46 +09:00
|
|
|
*/
|
2026-01-29 22:00:15 +09:00
|
|
|
public function getAnglePrice(string $searchOption): float
|
2026-01-29 19:30:46 +09:00
|
|
|
{
|
2026-01-29 22:00:15 +09:00
|
|
|
$cacheKey = "angle_bracket:{$searchOption}";
|
2026-01-29 19:30:46 +09:00
|
|
|
|
|
|
|
|
if (isset($this->cache[$cacheKey])) {
|
|
|
|
|
return $this->cache[$cacheKey];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$today = now()->toDateString();
|
|
|
|
|
|
|
|
|
|
$price = (float) (DB::table('items')
|
|
|
|
|
->join('item_details', 'item_details.item_id', '=', 'items.id')
|
|
|
|
|
->join('prices', 'prices.item_id', '=', 'items.id')
|
|
|
|
|
->where('items.tenant_id', $this->tenantId)
|
|
|
|
|
->where('items.is_active', true)
|
|
|
|
|
->whereNull('items.deleted_at')
|
2026-01-29 22:00:15 +09:00
|
|
|
->where('item_details.product_category', 'angle_bracket')
|
|
|
|
|
->where('item_details.part_type', $searchOption)
|
2026-01-29 19:30:46 +09:00
|
|
|
->where('prices.effective_from', '<=', $today)
|
|
|
|
|
->where(function ($q) use ($today) {
|
|
|
|
|
$q->whereNull('prices.effective_to')
|
|
|
|
|
->orWhere('prices.effective_to', '>=', $today);
|
|
|
|
|
})
|
|
|
|
|
->whereNull('prices.deleted_at')
|
|
|
|
|
->value('prices.sales_price') ?? 0);
|
|
|
|
|
|
|
|
|
|
$this->cache[$cacheKey] = $price;
|
|
|
|
|
|
2026-01-29 22:00:15 +09:00
|
|
|
return $price;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 부자재용 앵글 단가 (main angle)
|
|
|
|
|
*
|
|
|
|
|
* 5130: calculateMainAngle(1, itemList, '앵글3T', '2.5') → col4+col10 검색
|
|
|
|
|
*/
|
|
|
|
|
public function getMainAnglePrice(string $angleType, string $size): float
|
|
|
|
|
{
|
|
|
|
|
$cacheKey = "angle_main:{$angleType}:{$size}";
|
|
|
|
|
|
|
|
|
|
if (isset($this->cache[$cacheKey])) {
|
|
|
|
|
return $this->cache[$cacheKey];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$price = $this->getEstimatePartPriceBySpec('angle_main', $angleType, $size);
|
|
|
|
|
$this->cache[$cacheKey] = $price;
|
|
|
|
|
|
2026-01-29 19:30:46 +09:00
|
|
|
return $price;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 원자재 단가
|
|
|
|
|
*/
|
|
|
|
|
public function getRawMaterialPrice(string $materialName): float
|
|
|
|
|
{
|
|
|
|
|
return $this->getEstimatePartPrice('raw_material', $materialName);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
// 내부 헬퍼
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* product_category + part_type 기반 단가 조회
|
|
|
|
|
*/
|
|
|
|
|
private function getEstimatePartPrice(string $productCategory, string $partType): float
|
|
|
|
|
{
|
|
|
|
|
$cacheKey = "{$productCategory}:{$partType}";
|
|
|
|
|
|
|
|
|
|
if (isset($this->cache[$cacheKey])) {
|
|
|
|
|
return $this->cache[$cacheKey];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$today = now()->toDateString();
|
|
|
|
|
|
|
|
|
|
$price = (float) (DB::table('items')
|
|
|
|
|
->join('item_details', 'item_details.item_id', '=', 'items.id')
|
|
|
|
|
->join('prices', 'prices.item_id', '=', 'items.id')
|
|
|
|
|
->where('items.tenant_id', $this->tenantId)
|
|
|
|
|
->where('items.is_active', true)
|
|
|
|
|
->whereNull('items.deleted_at')
|
|
|
|
|
->where('item_details.product_category', $productCategory)
|
|
|
|
|
->where('item_details.part_type', $partType)
|
|
|
|
|
->where('prices.effective_from', '<=', $today)
|
|
|
|
|
->where(function ($q) use ($today) {
|
|
|
|
|
$q->whereNull('prices.effective_to')
|
|
|
|
|
->orWhere('prices.effective_to', '>=', $today);
|
|
|
|
|
})
|
|
|
|
|
->whereNull('prices.deleted_at')
|
|
|
|
|
->value('prices.sales_price') ?? 0);
|
|
|
|
|
|
|
|
|
|
$this->cache[$cacheKey] = $price;
|
|
|
|
|
|
|
|
|
|
return $price;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* product_category + spec1 + spec2 기반 단가 조회
|
|
|
|
|
*/
|
|
|
|
|
private function getEstimatePartPriceBySpec(string $productCategory, string $spec1, string $spec2): float
|
|
|
|
|
{
|
|
|
|
|
$cacheKey = "{$productCategory}:{$spec1}:{$spec2}";
|
|
|
|
|
|
|
|
|
|
if (isset($this->cache[$cacheKey])) {
|
|
|
|
|
return $this->cache[$cacheKey];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$today = now()->toDateString();
|
|
|
|
|
|
|
|
|
|
$price = (float) (DB::table('items')
|
|
|
|
|
->join('item_details', 'item_details.item_id', '=', 'items.id')
|
|
|
|
|
->join('prices', 'prices.item_id', '=', 'items.id')
|
|
|
|
|
->where('items.tenant_id', $this->tenantId)
|
|
|
|
|
->where('items.is_active', true)
|
|
|
|
|
->whereNull('items.deleted_at')
|
|
|
|
|
->where('item_details.product_category', $productCategory)
|
|
|
|
|
->where('item_details.part_type', $spec1)
|
|
|
|
|
->where('item_details.specification', $spec2)
|
|
|
|
|
->where('prices.effective_from', '<=', $today)
|
|
|
|
|
->where(function ($q) use ($today) {
|
|
|
|
|
$q->whereNull('prices.effective_to')
|
|
|
|
|
->orWhere('prices.effective_to', '>=', $today);
|
|
|
|
|
})
|
|
|
|
|
->whereNull('prices.deleted_at')
|
|
|
|
|
->value('prices.sales_price') ?? 0);
|
|
|
|
|
|
|
|
|
|
$this->cache[$cacheKey] = $price;
|
|
|
|
|
|
|
|
|
|
return $price;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 캐시 초기화
|
|
|
|
|
*/
|
|
|
|
|
public function clearCache(): void
|
|
|
|
|
{
|
|
|
|
|
$this->cache = [];
|
|
|
|
|
}
|
|
|
|
|
}
|