## 구현 내용 ### 모델 (5개) - QuoteFormulaCategory: 수식 카테고리 - QuoteFormula: 수식 정의 (input/calculation/range/mapping) - QuoteFormulaRange: 범위별 값 정의 - QuoteFormulaMapping: 매핑 테이블 - QuoteFormulaItem: 수식-품목 연결 ### 서비스 (3개) - QuoteFormulaCategoryService: 카테고리 CRUD - QuoteFormulaService: 수식 CRUD, 복제, 재정렬 - FormulaEvaluatorService: 수식 계산 엔진 - 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT ### API Controller (2개) - QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트) - QuoteFormulaController: 수식 API (16개 엔드포인트) ### FormRequest (4개) - Store/Update QuoteFormulaCategoryRequest - Store/Update QuoteFormulaRequest ### Blade Views (8개) - 수식 목록/추가/수정/시뮬레이터 - 카테고리 목록/추가/수정 - HTMX 테이블 partial ### 라우트 - API: 27개 엔드포인트 - Web: 7개 라우트
201 lines
5.9 KiB
PHP
201 lines
5.9 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Quote;
|
|
|
|
use App\Models\Quote\QuoteFormulaCategory;
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class QuoteFormulaCategoryService
|
|
{
|
|
/**
|
|
* 카테고리 목록 조회 (페이지네이션)
|
|
*/
|
|
public function getCategories(array $filters = [], int $perPage = 15): LengthAwarePaginator
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
$query = QuoteFormulaCategory::query()
|
|
->withCount('formulas')
|
|
->withCount(['formulas as active_formulas_count' => function ($q) {
|
|
$q->where('is_active', true);
|
|
}]);
|
|
|
|
// 테넌트 필터
|
|
if ($tenantId && $tenantId !== 'all') {
|
|
$query->where('tenant_id', $tenantId);
|
|
}
|
|
|
|
// 검색
|
|
if (! empty($filters['search'])) {
|
|
$search = $filters['search'];
|
|
$query->where(function ($q) use ($search) {
|
|
$q->where('name', 'like', "%{$search}%")
|
|
->orWhere('code', 'like', "%{$search}%")
|
|
->orWhere('description', 'like', "%{$search}%");
|
|
});
|
|
}
|
|
|
|
// 활성화 필터
|
|
if (isset($filters['is_active']) && $filters['is_active'] !== '') {
|
|
$query->where('is_active', (bool) $filters['is_active']);
|
|
}
|
|
|
|
// 정렬
|
|
$sortBy = $filters['sort_by'] ?? 'sort_order';
|
|
$sortDirection = $filters['sort_direction'] ?? 'asc';
|
|
$query->orderBy($sortBy, $sortDirection);
|
|
|
|
return $query->paginate($perPage);
|
|
}
|
|
|
|
/**
|
|
* 전체 카테고리 목록 (선택용)
|
|
*/
|
|
public function getAllCategories(): Collection
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
return QuoteFormulaCategory::query()
|
|
->where('tenant_id', $tenantId)
|
|
->where('is_active', true)
|
|
->orderBy('sort_order')
|
|
->get();
|
|
}
|
|
|
|
/**
|
|
* 활성 카테고리 목록 (드롭다운용)
|
|
*/
|
|
public function getActiveCategories(): Collection
|
|
{
|
|
return $this->getAllCategories();
|
|
}
|
|
|
|
/**
|
|
* 카테고리 상세 조회
|
|
*/
|
|
public function getCategoryById(int $id): ?QuoteFormulaCategory
|
|
{
|
|
return QuoteFormulaCategory::with('formulas')->find($id);
|
|
}
|
|
|
|
/**
|
|
* 카테고리 생성
|
|
*/
|
|
public function createCategory(array $data): QuoteFormulaCategory
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
return DB::transaction(function () use ($data, $tenantId) {
|
|
// 자동 sort_order 할당
|
|
if (! isset($data['sort_order'])) {
|
|
$maxOrder = QuoteFormulaCategory::where('tenant_id', $tenantId)->max('sort_order');
|
|
$data['sort_order'] = ($maxOrder ?? 0) + 1;
|
|
}
|
|
|
|
return QuoteFormulaCategory::create([
|
|
'tenant_id' => $tenantId,
|
|
'code' => $data['code'],
|
|
'name' => $data['name'],
|
|
'description' => $data['description'] ?? null,
|
|
'sort_order' => $data['sort_order'],
|
|
'is_active' => $data['is_active'] ?? true,
|
|
'created_by' => auth()->id(),
|
|
]);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 카테고리 수정
|
|
*/
|
|
public function updateCategory(int $id, array $data): QuoteFormulaCategory
|
|
{
|
|
$category = QuoteFormulaCategory::findOrFail($id);
|
|
|
|
$category->update([
|
|
'code' => $data['code'] ?? $category->code,
|
|
'name' => $data['name'] ?? $category->name,
|
|
'description' => array_key_exists('description', $data) ? $data['description'] : $category->description,
|
|
'sort_order' => $data['sort_order'] ?? $category->sort_order,
|
|
'is_active' => $data['is_active'] ?? $category->is_active,
|
|
'updated_by' => auth()->id(),
|
|
]);
|
|
|
|
return $category->fresh();
|
|
}
|
|
|
|
/**
|
|
* 카테고리 삭제
|
|
*/
|
|
public function deleteCategory(int $id): void
|
|
{
|
|
$category = QuoteFormulaCategory::findOrFail($id);
|
|
|
|
// 연관 수식이 있으면 삭제 불가
|
|
if ($category->formulas()->count() > 0) {
|
|
throw new \Exception('연관된 수식이 있어 삭제할 수 없습니다. 먼저 수식을 삭제해주세요.');
|
|
}
|
|
|
|
$category->delete();
|
|
}
|
|
|
|
/**
|
|
* 카테고리 순서 변경
|
|
*/
|
|
public function reorderCategories(array $orderData): void
|
|
{
|
|
DB::transaction(function () use ($orderData) {
|
|
foreach ($orderData as $item) {
|
|
QuoteFormulaCategory::where('id', $item['id'])
|
|
->update(['sort_order' => $item['sort_order']]);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 카테고리 활성/비활성 토글
|
|
*/
|
|
public function toggleActive(int $id): QuoteFormulaCategory
|
|
{
|
|
$category = QuoteFormulaCategory::findOrFail($id);
|
|
|
|
$category->is_active = ! $category->is_active;
|
|
$category->updated_by = auth()->id();
|
|
$category->save();
|
|
|
|
return $category;
|
|
}
|
|
|
|
/**
|
|
* 코드 중복 체크
|
|
*/
|
|
public function isCodeExists(string $code, ?int $excludeId = null): bool
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
$query = QuoteFormulaCategory::where('tenant_id', $tenantId)
|
|
->where('code', $code);
|
|
|
|
if ($excludeId) {
|
|
$query->where('id', '!=', $excludeId);
|
|
}
|
|
|
|
return $query->exists();
|
|
}
|
|
|
|
/**
|
|
* 카테고리 통계
|
|
*/
|
|
public function getCategoryStats(): array
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
return [
|
|
'total' => QuoteFormulaCategory::where('tenant_id', $tenantId)->count(),
|
|
'active' => QuoteFormulaCategory::where('tenant_id', $tenantId)->where('is_active', true)->count(),
|
|
'inactive' => QuoteFormulaCategory::where('tenant_id', $tenantId)->where('is_active', false)->count(),
|
|
];
|
|
}
|
|
}
|