diff --git a/CLAUDE.md b/CLAUDE.md index 0926253..efcca8d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -532,11 +532,42 @@ ### 6. i18n & Response Messages - Resource-specific keys allowed: message.product.created, message.bom.bulk_upsert ### 7. Swagger Documentation (l5-swagger 9.0) -- **Tags**: Resource-based (User, Auth, Product, BOM...) +- **Structure**: Swagger annotations are written in separate PHP class files under `app/Swagger/v1/` +- **File Naming**: `{Resource}Api.php` (e.g., CategoryApi.php, ClientApi.php, ProductApi.php) +- **Controller Clean**: Controllers contain ONLY business logic, NO Swagger annotations +- **Tags**: Resource-based (User, Auth, Product, BOM, Client...) - **Security**: ApiKeyAuth + BearerAuth -- **Schemas**: Reuse ApiResponse, ErrorResponse, resource DTOs +- **Schemas**: Define in Swagger files - Resource model, Pagination, CreateRequest, UpdateRequest +- **Methods**: Define empty methods for each endpoint with full @OA annotations - **Specifications**: Clear Path/Query/Body parameters with examples - **No duplicate schemas**, accurate nullable/oneOf distinctions +- **Regeneration**: Run `php artisan l5-swagger:generate` after creating/updating Swagger files + +**Swagger File Structure Example**: +```php +service = $service; + } + + public function index(Request $request) + { + return ApiResponse::handle(function () use ($request) { + $data = $this->service->index($request->all()); + return ['data' => $data, 'message' => __('message.fetched')]; + }); + } + + public function show(int $id) + { + return ApiResponse::handle(function () use ($id) { + $data = $this->service->show($id); + return ['data' => $data, 'message' => __('message.fetched')]; + }); + } + + public function store(Request $request) + { + return ApiResponse::handle(function () use ($request) { + $data = $this->service->store($request->all()); + return ['data' => $data, 'message' => __('message.created')]; + }); + } + + public function update(Request $request, int $id) + { + return ApiResponse::handle(function () use ($request, $id) { + $data = $this->service->update($id, $request->all()); + return ['data' => $data, 'message' => __('message.updated')]; + }); + } + + public function destroy(int $id) + { + return ApiResponse::handle(function () use ($id) { + $this->service->destroy($id); + return ['data' => null, 'message' => __('message.deleted')]; + }); + } + + public function toggle(int $id) + { + return ApiResponse::handle(function () use ($id) { + $data = $this->service->toggle($id); + return ['data' => $data, 'message' => __('message.updated')]; + }); + } +} diff --git a/app/Models/Commons/Category.php b/app/Models/Commons/Category.php index 64a9bbb..e1ff8ad 100644 --- a/app/Models/Commons/Category.php +++ b/app/Models/Commons/Category.php @@ -34,6 +34,9 @@ public function children() { return $this->hasMany(self::class, 'parent_id'); } // 카테고리의 제품들 public function products() { return $this->hasMany(\App\Models\Products\Product::class, 'category_id'); } + // 카테고리 필드 + public function categoryFields() { return $this->hasMany(CategoryField::class, 'category_id'); } + // 태그(폴리모픽) — 이미 taggables 존재 public function tags() { return $this->morphToMany(\App\Models\Commons\Tag::class, 'taggable'); } diff --git a/app/Models/Orders/Client.php b/app/Models/Orders/Client.php new file mode 100644 index 0000000..1ee3489 --- /dev/null +++ b/app/Models/Orders/Client.php @@ -0,0 +1,51 @@ + 'boolean', + ]; + + // ClientGroup 관계 + public function clientGroup() + { + return $this->belongsTo(ClientGroup::class, 'client_group_id'); + } + + // Orders 관계 + public function orders() + { + return $this->hasMany(Order::class, 'client_id'); + } + + // 스코프 + public function scopeActive($query) + { + return $query->where('is_active', 'Y'); + } + + public function scopeCode($query, string $code) + { + return $query->where('client_code', $code); + } +} diff --git a/app/Models/Orders/ClientGroup.php b/app/Models/Orders/ClientGroup.php new file mode 100644 index 0000000..48162b4 --- /dev/null +++ b/app/Models/Orders/ClientGroup.php @@ -0,0 +1,46 @@ + 'decimal:4', + 'is_active' => 'boolean', + ]; + + // Clients 관계 + public function clients() + { + return $this->hasMany(Client::class, 'client_group_id'); + } + + // 스코프 + public function scopeActive($query) + { + return $query->where('is_active', 1); + } + + public function scopeCode($query, string $code) + { + return $query->where('group_code', $code); + } +} diff --git a/app/Models/Products/PriceHistory.php b/app/Models/Products/PriceHistory.php index 87e19d5..f334ed8 100644 --- a/app/Models/Products/PriceHistory.php +++ b/app/Models/Products/PriceHistory.php @@ -2,6 +2,8 @@ namespace App\Models\Products; +use App\Models\Orders\ClientGroup; +use App\Traits\BelongsToTenant; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; @@ -10,6 +12,74 @@ */ class PriceHistory extends Model { - use SoftDeletes; - protected $fillable = ['tenant_id','item_type_code','item_id','price_type_code','price','started_at','ended_at']; + use BelongsToTenant, SoftDeletes; + + protected $fillable = [ + 'tenant_id', + 'item_type_code', + 'item_id', + 'price_type_code', + 'client_group_id', + 'price', + 'started_at', + 'ended_at', + 'created_by', + 'updated_by', + 'deleted_by', + ]; + + protected $casts = [ + 'price' => 'decimal:4', + 'started_at' => 'date', + 'ended_at' => 'date', + ]; + + // ClientGroup 관계 + public function clientGroup() + { + return $this->belongsTo(ClientGroup::class, 'client_group_id'); + } + + // Polymorphic 관계 (item_type_code에 따라 Product 또는 Material) + public function item() + { + if ($this->item_type_code === 'PRODUCT') { + return $this->belongsTo(Product::class, 'item_id'); + } elseif ($this->item_type_code === 'MATERIAL') { + return $this->belongsTo(\App\Models\Materials\Material::class, 'item_id'); + } + + return null; + } + + // 스코프 + public function scopeForItem($query, string $itemType, int $itemId) + { + return $query->where('item_type_code', $itemType) + ->where('item_id', $itemId); + } + + public function scopeForClientGroup($query, ?int $clientGroupId) + { + return $query->where('client_group_id', $clientGroupId); + } + + public function scopeValidAt($query, $date) + { + return $query->where('started_at', '<=', $date) + ->where(function ($q) use ($date) { + $q->whereNull('ended_at') + ->orWhere('ended_at', '>=', $date); + }); + } + + public function scopeSalePrice($query) + { + return $query->where('price_type_code', 'SALE'); + } + + public function scopePurchasePrice($query) + { + return $query->where('price_type_code', 'PURCHASE'); + } } diff --git a/app/Services/ClientService.php b/app/Services/ClientService.php new file mode 100644 index 0000000..fb61ed5 --- /dev/null +++ b/app/Services/ClientService.php @@ -0,0 +1,161 @@ +tenantId(); + + $page = (int)($params['page'] ?? 1); + $size = (int)($params['size'] ?? 20); + $q = trim((string)($params['q'] ?? '')); + $onlyActive = $params['only_active'] ?? null; + + $query = Client::query()->where('tenant_id', $tenantId); + + if ($q !== '') { + $query->where(function ($qq) use ($q) { + $qq->where('name', 'like', "%{$q}%") + ->orWhere('client_code', 'like', "%{$q}%") + ->orWhere('contact_person', 'like', "%{$q}%"); + }); + } + + if ($onlyActive !== null) { + $query->where('is_active', $onlyActive ? 'Y' : 'N'); + } + + $query->orderBy('client_code')->orderBy('id'); + + return $query->paginate($size, ['*'], 'page', $page); + } + + /** 단건 */ + public function show(int $id) + { + $tenantId = $this->tenantId(); + $client = Client::where('tenant_id', $tenantId)->find($id); + if (!$client) { + throw new NotFoundHttpException(__('error.not_found')); + } + return $client; + } + + /** 생성 */ + public function store(array $params) + { + $tenantId = $this->tenantId(); + $uid = $this->apiUserId(); + + $v = Validator::make($params, [ + 'client_code' => 'required|string|max:50', + 'name' => 'required|string|max:100', + 'contact_person' => 'nullable|string|max:50', + 'phone' => 'nullable|string|max:30', + 'email' => 'nullable|email|max:80', + 'address' => 'nullable|string|max:255', + 'is_active' => 'nullable|in:Y,N', + ]); + + if ($v->fails()) { + throw new BadRequestHttpException($v->errors()->first()); + } + + $data = $v->validated(); + + // client_code 중복 검사 + $exists = Client::where('tenant_id', $tenantId) + ->where('client_code', $data['client_code']) + ->exists(); + if ($exists) { + throw new BadRequestHttpException(__('error.duplicate_code')); + } + + $data['tenant_id'] = $tenantId; + $data['is_active'] = $data['is_active'] ?? 'Y'; + + return Client::create($data); + } + + /** 수정 */ + public function update(int $id, array $params) + { + $tenantId = $this->tenantId(); + $uid = $this->apiUserId(); + + $client = Client::where('tenant_id', $tenantId)->find($id); + if (!$client) { + throw new NotFoundHttpException(__('error.not_found')); + } + + $v = Validator::make($params, [ + 'client_code' => 'sometimes|required|string|max:50', + 'name' => 'sometimes|required|string|max:100', + 'contact_person' => 'nullable|string|max:50', + 'phone' => 'nullable|string|max:30', + 'email' => 'nullable|email|max:80', + 'address' => 'nullable|string|max:255', + 'is_active' => 'nullable|in:Y,N', + ]); + + if ($v->fails()) { + throw new BadRequestHttpException($v->errors()->first()); + } + + $payload = $v->validated(); + + // client_code 변경 시 중복 검사 + if (isset($payload['client_code']) && $payload['client_code'] !== $client->client_code) { + $exists = Client::where('tenant_id', $tenantId) + ->where('client_code', $payload['client_code']) + ->exists(); + if ($exists) { + throw new BadRequestHttpException(__('error.duplicate_code')); + } + } + + $client->update($payload); + return $client->refresh(); + } + + /** 삭제 */ + public function destroy(int $id) + { + $tenantId = $this->tenantId(); + + $client = Client::where('tenant_id', $tenantId)->find($id); + if (!$client) { + throw new NotFoundHttpException(__('error.not_found')); + } + + // 주문 존재 검사 + if ($client->orders()->exists()) { + throw new BadRequestHttpException(__('error.has_orders')); + } + + $client->delete(); + return 'success'; + } + + /** 활성/비활성 토글 */ + public function toggle(int $id) + { + $tenantId = $this->tenantId(); + $client = Client::where('tenant_id', $tenantId)->find($id); + if (!$client) { + throw new NotFoundHttpException(__('error.not_found')); + } + + $client->is_active = $client->is_active === 'Y' ? 'N' : 'Y'; + $client->save(); + return $client->refresh(); + } +} diff --git a/app/Services/Estimate/EstimateService.php b/app/Services/Estimate/EstimateService.php index 67e72e0..6a28bba 100644 --- a/app/Services/Estimate/EstimateService.php +++ b/app/Services/Estimate/EstimateService.php @@ -2,28 +2,32 @@ namespace App\Services\Estimate; -use App\Models\Commons\Category; use App\Models\Estimate\Estimate; use App\Models\Estimate\EstimateItem; -use App\Services\ModelSet\ModelSetService; -use App\Services\Service; use App\Services\Calculation\CalculationEngine; -use Illuminate\Support\Collection; -use Illuminate\Support\Facades\DB; +use App\Services\ModelSet\ModelSetService; +use App\Services\Pricing\PricingService; +use App\Services\Service; use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Support\Facades\DB; class EstimateService extends Service { protected ModelSetService $modelSetService; + protected CalculationEngine $calculationEngine; + protected PricingService $pricingService; + public function __construct( ModelSetService $modelSetService, - CalculationEngine $calculationEngine + CalculationEngine $calculationEngine, + PricingService $pricingService ) { parent::__construct(); $this->modelSetService = $modelSetService; $this->calculationEngine = $calculationEngine; + $this->pricingService = $pricingService; } /** @@ -35,37 +39,37 @@ public function getEstimates(array $filters = []): LengthAwarePaginator ->where('tenant_id', $this->tenantId()); // 필터링 - if (!empty($filters['status'])) { + if (! empty($filters['status'])) { $query->where('status', $filters['status']); } - if (!empty($filters['customer_name'])) { - $query->where('customer_name', 'like', '%' . $filters['customer_name'] . '%'); + if (! empty($filters['customer_name'])) { + $query->where('customer_name', 'like', '%'.$filters['customer_name'].'%'); } - if (!empty($filters['model_set_id'])) { + if (! empty($filters['model_set_id'])) { $query->where('model_set_id', $filters['model_set_id']); } - if (!empty($filters['date_from'])) { + if (! empty($filters['date_from'])) { $query->whereDate('created_at', '>=', $filters['date_from']); } - if (!empty($filters['date_to'])) { + if (! empty($filters['date_to'])) { $query->whereDate('created_at', '<=', $filters['date_to']); } - if (!empty($filters['search'])) { + if (! empty($filters['search'])) { $searchTerm = $filters['search']; $query->where(function ($q) use ($searchTerm) { - $q->where('estimate_name', 'like', '%' . $searchTerm . '%') - ->orWhere('estimate_no', 'like', '%' . $searchTerm . '%') - ->orWhere('project_name', 'like', '%' . $searchTerm . '%'); + $q->where('estimate_name', 'like', '%'.$searchTerm.'%') + ->orWhere('estimate_no', 'like', '%'.$searchTerm.'%') + ->orWhere('project_name', 'like', '%'.$searchTerm.'%'); }); } return $query->orderBy('created_at', 'desc') - ->paginate($filters['per_page'] ?? 20); + ->paginate($filters['per_page'] ?? 20); } /** @@ -110,15 +114,22 @@ public function createEstimate(array $data): array 'parameters' => $data['parameters'], 'calculated_results' => $bomCalculation['calculated_values'] ?? [], 'bom_data' => $bomCalculation, - 'total_amount' => $bomCalculation['total_amount'] ?? 0, + 'total_amount' => 0, // 항목 생성 후 재계산 'notes' => $data['notes'] ?? null, 'valid_until' => now()->addDays(30), // 기본 30일 유효 'created_by' => $this->apiUserId(), ]); - // 견적 항목 생성 (BOM 기반) - if (!empty($bomCalculation['bom_items'])) { - $this->createEstimateItems($estimate, $bomCalculation['bom_items']); + // 견적 항목 생성 (BOM 기반) + 가격 계산 + if (! empty($bomCalculation['bom_items'])) { + $totalAmount = $this->createEstimateItems( + $estimate, + $bomCalculation['bom_items'], + $data['client_id'] ?? null + ); + + // 총액 업데이트 + $estimate->update(['total_amount' => $totalAmount]); } return $this->getEstimateDetail($estimate->id); @@ -143,12 +154,16 @@ public function updateEstimate($estimateId, array $data): array $data['calculated_results'] = $bomCalculation['calculated_values'] ?? []; $data['bom_data'] = $bomCalculation; - $data['total_amount'] = $bomCalculation['total_amount'] ?? 0; // 기존 견적 항목 삭제 후 재생성 $estimate->items()->delete(); - if (!empty($bomCalculation['bom_items'])) { - $this->createEstimateItems($estimate, $bomCalculation['bom_items']); + if (! empty($bomCalculation['bom_items'])) { + $totalAmount = $this->createEstimateItems( + $estimate, + $bomCalculation['bom_items'], + $data['client_id'] ?? null + ); + $data['total_amount'] = $totalAmount; } } @@ -249,13 +264,13 @@ public function changeEstimateStatus($estimateId, string $status, ?string $notes 'EXPIRED' => ['DRAFT'], ]; - if (!in_array($status, $validTransitions[$estimate->status] ?? [])) { + if (! in_array($status, $validTransitions[$estimate->status] ?? [])) { throw new \Exception(__('error.estimate.invalid_status_transition')); } $estimate->update([ 'status' => $status, - 'notes' => $notes ? ($estimate->notes . "\n\n" . $notes) : $estimate->notes, + 'notes' => $notes ? ($estimate->notes."\n\n".$notes) : $estimate->notes, 'updated_by' => $this->apiUserId(), ]); @@ -288,25 +303,63 @@ public function previewCalculation($modelSetId, array $parameters): array } /** - * 견적 항목 생성 + * 견적 항목 생성 (가격 계산 포함) + * + * @return float 총 견적 금액 */ - protected function createEstimateItems(Estimate $estimate, array $bomItems): void + protected function createEstimateItems(Estimate $estimate, array $bomItems, ?int $clientId = null): float { + $totalAmount = 0; + $warnings = []; + foreach ($bomItems as $index => $bomItem) { + $quantity = $bomItem['quantity'] ?? 1; + $unitPrice = 0; + + // 가격 조회 (item_id와 item_type이 있는 경우) + if (isset($bomItem['item_id']) && isset($bomItem['item_type'])) { + $priceResult = $this->pricingService->getItemPrice( + $bomItem['item_type'], // 'PRODUCT' or 'MATERIAL' + $bomItem['item_id'], + $clientId, + now()->format('Y-m-d') + ); + + $unitPrice = $priceResult['price'] ?? 0; + + if ($priceResult['warning']) { + $warnings[] = $priceResult['warning']; + } + } + + $totalPrice = $unitPrice * $quantity; + $totalAmount += $totalPrice; + EstimateItem::create([ 'tenant_id' => $this->tenantId(), 'estimate_id' => $estimate->id, 'sequence' => $index + 1, - 'item_name' => $bomItem['name'] ?? '견적 항목 ' . ($index + 1), + 'item_name' => $bomItem['name'] ?? '견적 항목 '.($index + 1), 'item_description' => $bomItem['description'] ?? '', 'parameters' => $bomItem['parameters'] ?? [], 'calculated_values' => $bomItem['calculated_values'] ?? [], - 'unit_price' => $bomItem['unit_price'] ?? 0, - 'quantity' => $bomItem['quantity'] ?? 1, + 'unit_price' => $unitPrice, + 'quantity' => $quantity, + 'total_price' => $totalPrice, 'bom_components' => $bomItem['components'] ?? [], 'created_by' => $this->apiUserId(), ]); } + + // 가격 경고가 있으면 로그 기록 + if (! empty($warnings)) { + \Log::warning('견적 가격 조회 경고', [ + 'estimate_id' => $estimate->id, + 'warnings' => $warnings, + ]); + } + + return $totalAmount; } /** @@ -321,19 +374,19 @@ protected function summarizeCalculations(Estimate $estimate): array ]; // 주요 계산 결과 추출 - if (!empty($estimate->calculated_results)) { + if (! empty($estimate->calculated_results)) { $results = $estimate->calculated_results; if (isset($results['W1'], $results['H1'])) { - $summary['key_calculations']['제작사이즈'] = $results['W1'] . ' × ' . $results['H1'] . ' mm'; + $summary['key_calculations']['제작사이즈'] = $results['W1'].' × '.$results['H1'].' mm'; } if (isset($results['weight'])) { - $summary['key_calculations']['중량'] = $results['weight'] . ' kg'; + $summary['key_calculations']['중량'] = $results['weight'].' kg'; } if (isset($results['area'])) { - $summary['key_calculations']['면적'] = $results['area'] . ' ㎡'; + $summary['key_calculations']['면적'] = $results['area'].' ㎡'; } if (isset($results['bracket_size'])) { @@ -343,4 +396,4 @@ protected function summarizeCalculations(Estimate $estimate): array return $summary; } -} \ No newline at end of file +} diff --git a/app/Services/Pricing/PricingService.php b/app/Services/Pricing/PricingService.php new file mode 100644 index 0000000..0989db3 --- /dev/null +++ b/app/Services/Pricing/PricingService.php @@ -0,0 +1,196 @@ + float|null, 'price_history_id' => int|null, 'client_group_id' => int|null, 'warning' => string|null] + */ + public function getItemPrice(string $itemType, int $itemId, ?int $clientId = null, ?string $date = null): array + { + $date = $date ?? Carbon::today()->format('Y-m-d'); + $clientGroupId = null; + + // 1. 고객의 그룹 ID 확인 + if ($clientId) { + $client = Client::where('tenant_id', $this->tenantId()) + ->where('id', $clientId) + ->first(); + + if ($client) { + $clientGroupId = $client->client_group_id; + } + } + + // 2. 가격 조회 (우선순위대로) + $priceHistory = null; + + // 1순위: 고객 그룹별 매출단가 + if ($clientGroupId) { + $priceHistory = $this->findPrice($itemType, $itemId, $clientGroupId, $date); + } + + // 2순위: 기본 매출단가 (client_group_id = NULL) + if (! $priceHistory) { + $priceHistory = $this->findPrice($itemType, $itemId, null, $date); + } + + // 3순위: NULL (경고) + if (! $priceHistory) { + return [ + 'price' => null, + 'price_history_id' => null, + 'client_group_id' => null, + 'warning' => __('error.price_not_found', [ + 'item_type' => $itemType, + 'item_id' => $itemId, + 'date' => $date, + ]), + ]; + } + + return [ + 'price' => (float) $priceHistory->price, + 'price_history_id' => $priceHistory->id, + 'client_group_id' => $priceHistory->client_group_id, + 'warning' => null, + ]; + } + + /** + * 가격 이력에서 유효한 가격 조회 + */ + private function findPrice(string $itemType, int $itemId, ?int $clientGroupId, string $date): ?PriceHistory + { + return PriceHistory::where('tenant_id', $this->tenantId()) + ->forItem($itemType, $itemId) + ->forClientGroup($clientGroupId) + ->salePrice() + ->validAt($date) + ->orderBy('started_at', 'desc') + ->first(); + } + + /** + * 여러 항목의 단가를 일괄 조회 + * + * @param array $items [['item_type' => 'PRODUCT', 'item_id' => 1], ...] + * @return array ['prices' => [...], 'warnings' => [...]] + */ + public function getBulkItemPrices(array $items, ?int $clientId = null, ?string $date = null): array + { + $prices = []; + $warnings = []; + + foreach ($items as $item) { + $result = $this->getItemPrice( + $item['item_type'], + $item['item_id'], + $clientId, + $date + ); + + $prices[] = array_merge($item, [ + 'price' => $result['price'], + 'price_history_id' => $result['price_history_id'], + 'client_group_id' => $result['client_group_id'], + ]); + + if ($result['warning']) { + $warnings[] = $result['warning']; + } + } + + return [ + 'prices' => $prices, + 'warnings' => $warnings, + ]; + } + + /** + * 가격 등록/수정 + */ + public function upsertPrice(array $data): PriceHistory + { + $data['tenant_id'] = $this->tenantId(); + $data['created_by'] = $this->apiUserId(); + $data['updated_by'] = $this->apiUserId(); + + // 중복 확인: 동일 조건(item, client_group, date 범위)의 가격이 이미 있는지 + $existing = PriceHistory::where('tenant_id', $data['tenant_id']) + ->where('item_type_code', $data['item_type_code']) + ->where('item_id', $data['item_id']) + ->where('price_type_code', $data['price_type_code']) + ->where('client_group_id', $data['client_group_id'] ?? null) + ->where('started_at', $data['started_at']) + ->first(); + + if ($existing) { + $existing->update($data); + + return $existing->fresh(); + } + + return PriceHistory::create($data); + } + + /** + * 가격 이력 조회 (페이지네이션) + * + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator + */ + public function listPrices(array $filters = [], int $perPage = 15) + { + $query = PriceHistory::where('tenant_id', $this->tenantId()); + + if (isset($filters['item_type_code'])) { + $query->where('item_type_code', $filters['item_type_code']); + } + + if (isset($filters['item_id'])) { + $query->where('item_id', $filters['item_id']); + } + + if (isset($filters['price_type_code'])) { + $query->where('price_type_code', $filters['price_type_code']); + } + + if (isset($filters['client_group_id'])) { + $query->where('client_group_id', $filters['client_group_id']); + } + + if (isset($filters['date'])) { + $query->validAt($filters['date']); + } + + return $query->orderBy('started_at', 'desc') + ->orderBy('created_at', 'desc') + ->paginate($perPage); + } + + /** + * 가격 삭제 (Soft Delete) + */ + public function deletePrice(int $id): bool + { + $price = PriceHistory::where('tenant_id', $this->tenantId()) + ->findOrFail($id); + + $price->deleted_by = $this->apiUserId(); + $price->save(); + + return $price->delete(); + } +} diff --git a/app/Swagger/v1/ClientApi.php b/app/Swagger/v1/ClientApi.php new file mode 100644 index 0000000..4f70d5b --- /dev/null +++ b/app/Swagger/v1/ClientApi.php @@ -0,0 +1,185 @@ +id(); + $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); + $table->string('group_code', 30)->comment('그룹 코드'); + $table->string('group_name', 100)->comment('그룹명'); + $table->decimal('price_rate', 5, 4)->default(1.0000)->comment('가격 배율 (1.0 = 기준가, 0.9 = 90%, 1.1 = 110%)'); + $table->tinyInteger('is_active')->default(1)->comment('활성 여부 (1=활성, 0=비활성)'); + + // 감사 컬럼 + $table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID'); + $table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID'); + $table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자 ID'); + + $table->timestamps(); + $table->softDeletes(); + + // 인덱스 + $table->index(['tenant_id', 'is_active']); + $table->unique(['tenant_id', 'group_code'], 'uq_client_groups_tenant_code'); + + // 외래키 + $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('client_groups'); + } +}; diff --git a/database/migrations/2025_10_13_213556_add_client_group_id_to_clients_table.php b/database/migrations/2025_10_13_213556_add_client_group_id_to_clients_table.php new file mode 100644 index 0000000..079b057 --- /dev/null +++ b/database/migrations/2025_10_13_213556_add_client_group_id_to_clients_table.php @@ -0,0 +1,36 @@ +unsignedBigInteger('client_group_id') + ->nullable() + ->after('tenant_id') + ->comment('고객 그룹 ID (NULL = 기본 그룹)'); + + $table->index(['tenant_id', 'client_group_id']); + $table->foreign('client_group_id')->references('id')->on('client_groups')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('clients', function (Blueprint $table) { + $table->dropForeign(['client_group_id']); + $table->dropIndex(['tenant_id', 'client_group_id']); + $table->dropColumn('client_group_id'); + }); + } +}; diff --git a/database/migrations/2025_10_13_213602_add_client_group_id_to_price_histories_table.php b/database/migrations/2025_10_13_213602_add_client_group_id_to_price_histories_table.php new file mode 100644 index 0000000..e7f225a --- /dev/null +++ b/database/migrations/2025_10_13_213602_add_client_group_id_to_price_histories_table.php @@ -0,0 +1,40 @@ +unsignedBigInteger('client_group_id') + ->nullable() + ->after('price_type_code') + ->comment('고객 그룹 ID (NULL = 기본 가격, 값 있으면 그룹별 차등 가격)'); + + // 기존 인덱스에 client_group_id 추가 + $table->dropIndex('idx_price_histories_main'); + $table->index(['tenant_id', 'item_type_code', 'item_id', 'client_group_id', 'started_at'], 'idx_price_histories_main'); + + $table->foreign('client_group_id')->references('id')->on('client_groups')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('price_histories', function (Blueprint $table) { + $table->dropForeign(['client_group_id']); + $table->dropIndex('idx_price_histories_main'); + $table->index(['tenant_id', 'item_type_code', 'item_id', 'started_at'], 'idx_price_histories_main'); + $table->dropColumn('client_group_id'); + }); + } +}; diff --git a/lang/ko/error.php b/lang/ko/error.php index 24c3241..b6d96c9 100644 --- a/lang/ko/error.php +++ b/lang/ko/error.php @@ -67,4 +67,7 @@ 'invalid_file_type' => '허용되지 않는 파일 형식입니다.', 'file_too_large' => '파일 크기가 너무 큽니다.', ], + + // 가격 관리 관련 + 'price_not_found' => ':item_type ID :item_id 항목의 :date 기준 매출단가를 찾을 수 없습니다.', ]; diff --git a/relationships.txt b/relationships.txt new file mode 100644 index 0000000..b62fe02 --- /dev/null +++ b/relationships.txt @@ -0,0 +1,2502 @@ +digraph "G" { +style="filled" +bgcolor="#F7F7F7" +fontsize="12" +labelloc="t" +concentrate="1" +splines="polyline" +overlap="" +nodesep="1" +rankdir="LR" +pad="0.5" +ranksep="2" +esep="1" +fontname="Helvetica Neue" +appmodelsboardsboard:id -> appmodelsboardsboardsetting:board_id [ +label=" " +xlabel="HasMany +customFields" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelsboardsboard:id -> appmodelsboardspost:board_id [ +label=" " +xlabel="HasMany +posts" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelsboardsboardcomment:post_id -> appmodelsboardspost:id [ +label=" " +xlabel="BelongsTo +post" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsboardsboardcomment:user_id -> appmodelsmembersuser:id [ +label=" " +xlabel="BelongsTo +user" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsboardsboardcomment:parent_id -> appmodelsboardsboardcomment:id [ +label=" " +xlabel="BelongsTo +parent" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsboardsboardcomment:id -> appmodelsboardsboardcomment:parent_id [ +label=" " +xlabel="HasMany +children" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelsboardsboardsetting:board_id -> appmodelsboardsboard:id [ +label=" " +xlabel="BelongsTo +board" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsboardspost:id -> appmodelscommonsfile:fileable_id [ +label=" " +xlabel="MorphMany +files" +color="#003049" +penwidth="1.8" +fontname="Helvetica Neue" +] +appmodelsboardspost:id -> appmodelsboardsboardcomment:post_id [ +label=" " +xlabel="HasMany +comments" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelsboardspost:board_id -> appmodelsboardsboard:id [ +label=" " +xlabel="BelongsTo +board" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsboardspostcustomfieldvalue:post_id -> appmodelsboardspost:id [ +label=" " +xlabel="BelongsTo +post" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsboardspostcustomfieldvalue:field_id -> appmodelsboardsboardsetting:id [ +label=" " +xlabel="BelongsTo +field" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelscommonscategory:parent_id -> appmodelscommonscategory:id [ +label=" " +xlabel="BelongsTo +parent" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelscommonscategory:id -> appmodelscommonscategory:parent_id [ +label=" " +xlabel="HasMany +children" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelscommonscategory:id -> appmodelsproductsproduct:category_id [ +label=" " +xlabel="HasMany +products" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelscommonscategory -> appmodelscommonstag [ +label=" " +xlabel="MorphToMany +tags" +color="#003049" +penwidth="1.8" +fontname="Helvetica Neue" +] +appmodelscommonscategoryfield:category_id -> appmodelscommonscategory:id [ +label=" " +xlabel="BelongsTo +category" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelscommonscategorylog:category_id -> appmodelscommonscategory:id [ +label=" " +xlabel="BelongsTo +category" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelscommonscategorytemplate:category_id -> appmodelscommonscategory:id [ +label=" " +xlabel="BelongsTo +category" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelscommonsfile:fileable_id -> appmodelscommonsfile [ +label=" " +xlabel="MorphTo +fileable" +color="#003049" +penwidth="1.8" +fontname="Helvetica Neue" +] +appmodelscommonsfile:uploaded_by -> appmodelsmembersuser:id [ +label=" " +xlabel="BelongsTo +uploader" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelscommonsmenu:parent_id -> appmodelscommonsmenu:id [ +label=" " +xlabel="BelongsTo +parent" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelscommonsmenu:id -> appmodelscommonsmenu:parent_id [ +label=" " +xlabel="HasMany +children" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelscommonstag:tenant_id -> appmodelstenantstenant:id [ +label=" " +xlabel="BelongsTo +tenant" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelscommonstag -> appmodelsproductsproduct [ +label=" " +xlabel="MorphToMany +products" +color="#003049" +penwidth="1.8" +fontname="Helvetica Neue" +] +appmodelscommonstag -> appmodelsproductspart [ +label=" " +xlabel="MorphToMany +parts" +color="#003049" +penwidth="1.8" +fontname="Helvetica Neue" +] +appmodelscommonstag -> appmodelsmaterialsmaterial [ +label=" " +xlabel="MorphToMany +materials" +color="#003049" +penwidth="1.8" +fontname="Helvetica Neue" +] +appmodelsdesignbomtemplate:model_version_id -> appmodelsdesignmodelversion:id [ +label=" " +xlabel="BelongsTo +modelVersion" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsdesignbomtemplate:id -> appmodelsdesignbomtemplateitem:bom_template_id [ +label=" " +xlabel="HasMany +items" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelsdesignbomtemplateitem:bom_template_id -> appmodelsdesignbomtemplate:id [ +label=" " +xlabel="BelongsTo +template" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsdesigndesignmodel:id -> appmodelsdesignmodelversion:model_id [ +label=" " +xlabel="HasMany +versions" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelsdesignmodelversion:model_id -> appmodelsdesigndesignmodel:id [ +label=" " +xlabel="BelongsTo +model" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsdesignmodelversion:id -> appmodelsdesignbomtemplate:model_version_id [ +label=" " +xlabel="HasMany +bomTemplates" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelsestimateestimate:model_set_id -> appmodelscommonscategory:id [ +label=" " +xlabel="BelongsTo +modelSet" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsestimateestimate:id -> appmodelsestimateestimateitem:estimate_id [ +label=" " +xlabel="HasMany +items" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelsestimateestimateitem:estimate_id -> appmodelsestimateestimate:id [ +label=" " +xlabel="BelongsTo +estimate" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsmainrequest:id -> appmodelsmainrequestflow:main_request_id [ +label=" " +xlabel="HasMany +flows" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelsmainrequestflow:main_request_id -> appmodelsmainrequest:id [ +label=" " +xlabel="BelongsTo +mainRequest" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsmainrequestflow:flowable_id -> appmodelsmainrequestflow [ +label=" " +xlabel="MorphTo +flowable" +color="#003049" +penwidth="1.8" +fontname="Helvetica Neue" +] +appmodelsmaterialsmaterial:id -> appmodelsmaterialsmaterialreceipt:material_id [ +label=" " +xlabel="HasMany +receipts" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelsmaterialsmaterial:id -> appmodelsqualityslot:material_id [ +label=" " +xlabel="HasMany +lots" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelsmaterialsmaterial:id -> appmodelscommonsfile:fileable_id [ +label=" " +xlabel="MorphMany +files" +color="#003049" +penwidth="1.8" +fontname="Helvetica Neue" +] +appmodelsmaterialsmaterial -> appmodelscommonstag [ +label=" " +xlabel="MorphToMany +tags" +color="#003049" +penwidth="1.8" +fontname="Helvetica Neue" +] +appmodelsmaterialsmaterialinspection:receipt_id -> appmodelsmaterialsmaterialreceipt:id [ +label=" " +xlabel="BelongsTo +receipt" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsmaterialsmaterialinspection:id -> appmodelsmaterialsmaterialinspectionitem:inspection_id [ +label=" " +xlabel="HasMany +items" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelsmaterialsmaterialinspectionitem:inspection_id -> appmodelsmaterialsmaterialinspection:id [ +label=" " +xlabel="BelongsTo +inspection" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsmaterialsmaterialreceipt:material_id -> appmodelsmaterialsmaterial:id [ +label=" " +xlabel="BelongsTo +material" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsmaterialsmaterialreceipt:id -> appmodelsmaterialsmaterialinspection:receipt_id [ +label=" " +xlabel="HasMany +inspections" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelsmembersuser:id -> appmodelsmembersusertenant:user_id [ +label=" " +xlabel="HasMany +userTenants" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelsmembersuser:id -> appmodelsmembersusertenant:user_id [ +label=" " +xlabel="HasOne +userTenant" +color="#D62828" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="none" +] +appmodelsmembersuser:id -> appmodelsmembersuserrole:user_id [ +label=" " +xlabel="HasMany +userRoles" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelsmembersuser:id -> appmodelscommonsfile:fileable_id [ +label=" " +xlabel="MorphMany +files" +color="#003049" +penwidth="1.8" +fontname="Helvetica Neue" +] +appmodelsmembersusermenupermission:user_id -> appmodelsmembersuser:id [ +label=" " +xlabel="BelongsTo +user" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsmembersusermenupermission:menu_id -> appmodelscommonsmenu:id [ +label=" " +xlabel="BelongsTo +menu" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsmembersuserrole:user_id -> appmodelsmembersuser:id [ +label=" " +xlabel="BelongsTo +user" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsmembersuserrole:tenant_id -> appmodelstenantstenant:id [ +label=" " +xlabel="BelongsTo +tenant" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsmembersuserrole:role_id -> appmodelspermissionsrole:id [ +label=" " +xlabel="BelongsTo +role" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsmembersusertenant:user_id -> appmodelsmembersuser:id [ +label=" " +xlabel="BelongsTo +user" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsmembersusertenant:tenant_id -> appmodelstenantstenant:id [ +label=" " +xlabel="BelongsTo +tenant" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsordersorder:id -> appmodelsordersorderitem:order_id [ +label=" " +xlabel="HasMany +items" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelsordersorder:id -> appmodelsordersorderhistory:order_id [ +label=" " +xlabel="HasMany +histories" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelsordersorder:id -> appmodelsordersorderversion:order_id [ +label=" " +xlabel="HasMany +versions" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelsordersorderhistory:order_id -> appmodelsordersorder:id [ +label=" " +xlabel="BelongsTo +order" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsordersorderitem:id -> appmodelsordersorderitemcomponent:order_item_id [ +label=" " +xlabel="HasMany +components" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelsordersorderitem:order_id -> appmodelsordersorder:id [ +label=" " +xlabel="BelongsTo +order" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsordersorderitemcomponent:order_item_id -> appmodelsordersorderitem:id [ +label=" " +xlabel="BelongsTo +orderItem" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsordersorderversion:order_id -> appmodelsordersorder:id [ +label=" " +xlabel="BelongsTo +order" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelspermissionspermissionoverride:model_id -> appmodelspermissionspermissionoverride [ +label=" " +xlabel="MorphTo +model" +color="#003049" +penwidth="1.8" +fontname="Helvetica Neue" +] +appmodelspermissionsrole:id -> appmodelspermissionsrolemenupermission:role_id [ +label=" " +xlabel="HasMany +menuPermissions" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelspermissionsrole:tenant_id -> appmodelstenantstenant:id [ +label=" " +xlabel="BelongsTo +tenant" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelspermissionsrole:id -> appmodelsmembersuserrole:role_id [ +label=" " +xlabel="HasMany +userRoles" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelspermissionsrolemenupermission:role_id -> appmodelspermissionsrole:id [ +label=" " +xlabel="BelongsTo +role" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelspermissionsrolemenupermission:menu_id -> appmodelscommonsmenu:id [ +label=" " +xlabel="BelongsTo +menu" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsproductscommoncode:parent_id -> appmodelsproductscommoncode:id [ +label=" " +xlabel="BelongsTo +parent" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsproductscommoncode:id -> appmodelsproductscommoncode:parent_id [ +label=" " +xlabel="HasMany +children" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelsproductspart:category_id -> appmodelsproductscommoncode:id [ +label=" " +xlabel="BelongsTo +category" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsproductspart:part_type_id -> appmodelsproductscommoncode:id [ +label=" " +xlabel="BelongsTo +partType" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsproductspart -> appmodelscommonstag [ +label=" " +xlabel="MorphToMany +tags" +color="#003049" +penwidth="1.8" +fontname="Helvetica Neue" +] +appmodelsproductsproduct:category_id -> appmodelscommonscategory:id [ +label=" " +xlabel="BelongsTo +category" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsproductsproduct:id -> appmodelsproductsproductcomponent:parent_product_id [ +label=" " +xlabel="HasMany +componentLines" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelsproductsproduct:id -> appmodelsproductsproductcomponent:child_product_id [ +label=" " +xlabel="HasMany +parentLines" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelsproductsproduct:id -> product_components:parent_product_id [ +label=" " +xlabel="BelongsToMany +children" +color="#003049" +penwidth="1.8" +fontname="Helvetica Neue" +] +product_components:child_product_id -> appmodelsproductsproduct:id [ +label=" " +xlabel="BelongsToMany +children" +color="#003049" +penwidth="1.8" +fontname="Helvetica Neue" +] +appmodelsproductsproduct:id -> product_components:child_product_id [ +label=" " +xlabel="BelongsToMany +parents" +color="#003049" +penwidth="1.8" +fontname="Helvetica Neue" +] +product_components:parent_product_id -> appmodelsproductsproduct:id [ +label=" " +xlabel="BelongsToMany +parents" +color="#003049" +penwidth="1.8" +fontname="Helvetica Neue" +] +appmodelsproductsproduct:id -> appmodelscommonsfile:fileable_id [ +label=" " +xlabel="MorphMany +files" +color="#003049" +penwidth="1.8" +fontname="Helvetica Neue" +] +appmodelsproductsproduct -> appmodelscommonstag [ +label=" " +xlabel="MorphToMany +tags" +color="#003049" +penwidth="1.8" +fontname="Helvetica Neue" +] +appmodelsproductsproductcomponent:parent_product_id -> appmodelsproductsproduct:id [ +label=" " +xlabel="BelongsTo +parentProduct" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsproductsproductcomponent:child_product_id -> appmodelsproductsproduct:id [ +label=" " +xlabel="BelongsTo +childProduct" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsproductsproductcomponent:material_id -> appmodelsmaterialsmaterial:id [ +label=" " +xlabel="BelongsTo +material" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsqualityslot:material_id -> appmodelsmaterialsmaterial:id [ +label=" " +xlabel="BelongsTo +material" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelsqualityslot:id -> appmodelsqualityslotsale:lot_id [ +label=" " +xlabel="HasMany +sales" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelsqualityslotsale:lot_id -> appmodelsqualityslot:id [ +label=" " +xlabel="BelongsTo +lot" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelstenantsdepartment:parent_id -> appmodelstenantsdepartment:id [ +label=" " +xlabel="BelongsTo +parent" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelstenantsdepartment:id -> appmodelstenantsdepartment:parent_id [ +label=" " +xlabel="HasMany +children" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelstenantsdepartment:id -> department_user:department_id [ +label=" " +xlabel="BelongsToMany +users" +color="#003049" +penwidth="1.8" +fontname="Helvetica Neue" +] +department_user:user_id -> appmodelsmembersuser:id [ +label=" " +xlabel="BelongsToMany +users" +color="#003049" +penwidth="1.8" +fontname="Helvetica Neue" +] +appmodelstenantsdepartment:id -> appmodelspermissionspermissionoverride:model_id [ +label=" " +xlabel="MorphMany +permissionOverrides" +color="#003049" +penwidth="1.8" +fontname="Helvetica Neue" +] +appmodelstenantsdepartment:id -> appmodelstenantspivotsdepartmentuser:department_id [ +label=" " +xlabel="HasMany +departmentUsers" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelstenantspayment:subscription_id -> appmodelstenantssubscription:id [ +label=" " +xlabel="BelongsTo +subscription" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelstenantspivotsdepartmentuser:department_id -> appmodelstenantsdepartment:id [ +label=" " +xlabel="BelongsTo +department" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelstenantspivotsdepartmentuser:user_id -> appmodelsmembersuser:id [ +label=" " +xlabel="BelongsTo +user" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelstenantsplan:id -> appmodelstenantssubscription:plan_id [ +label=" " +xlabel="HasMany +subscriptions" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelstenantssettingfielddef:field_key -> appmodelstenantstenantfieldsetting:field_key [ +label=" " +xlabel="HasMany +tenantSettings" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelstenantssubscription:tenant_id -> appmodelstenantstenant:id [ +label=" " +xlabel="BelongsTo +tenant" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelstenantssubscription:plan_id -> appmodelstenantsplan:id [ +label=" " +xlabel="BelongsTo +plan" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelstenantssubscription:id -> appmodelstenantspayment:subscription_id [ +label=" " +xlabel="HasMany +payments" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelstenantstenant:plan_id -> appmodelstenantsplan:id [ +label=" " +xlabel="BelongsTo +plan" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelstenantstenant:subscription_id -> appmodelstenantssubscription:id [ +label=" " +xlabel="BelongsTo +subscription" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelstenantstenant:id -> appmodelsmembersusertenant:tenant_id [ +label=" " +xlabel="HasMany +userTenants" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelstenantstenant:id -> user_tenants:tenant_id [ +label=" " +xlabel="BelongsToMany +users" +color="#003049" +penwidth="1.8" +fontname="Helvetica Neue" +] +user_tenants:user_id -> appmodelsmembersuser:id [ +label=" " +xlabel="BelongsToMany +users" +color="#003049" +penwidth="1.8" +fontname="Helvetica Neue" +] +appmodelstenantstenant:id -> appmodelspermissionsrole:tenant_id [ +label=" " +xlabel="HasMany +roles" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelstenantstenant:id -> appmodelsmembersuserrole:tenant_id [ +label=" " +xlabel="HasMany +userRoles" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelstenantstenant:id -> appmodelscommonsfile:fileable_id [ +label=" " +xlabel="MorphMany +files" +color="#003049" +penwidth="1.8" +fontname="Helvetica Neue" +] +appmodelstenantstenantfieldsetting:field_key -> appmodelstenantssettingfielddef:field_key [ +label=" " +xlabel="BelongsTo +fieldDef" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelstenantstenantfieldsetting:option_group_id -> appmodelstenantstenantoptiongroup:id [ +label=" " +xlabel="BelongsTo +optionGroup" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelstenantstenantoptiongroup:id -> appmodelstenantstenantoptionvalue:group_id [ +label=" " +xlabel="HasMany +values" +color="#FCBF49" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="crow" +arrowtail="none" +] +appmodelstenantstenantoptionvalue:group_id -> appmodelstenantstenantoptiongroup:id [ +label=" " +xlabel="BelongsTo +group" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelstenantstenantuserprofile:user_id -> appmodelsmembersuser:id [ +label=" " +xlabel="BelongsTo +user" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +appmodelstenantstenantuserprofile:department_id -> appmodelstenantsdepartment:id [ +label=" " +xlabel="BelongsTo +department" +color="#F77F00" +penwidth="1.8" +fontname="Helvetica Neue" +dir="both" +arrowhead="tee" +arrowtail="crow" +] +"appmodelsapikey" [ +label=< + + + + + + + +
ApiKey
id (bigint)
key (varchar)
description (varchar)
is_active (tinyint)
created_at (timestamp)
updated_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsauditauditlog" [ +label=< + + + + + + + + + + + + +
AuditLog
id (bigint)
tenant_id (bigint)
target_type (varchar)
target_id (bigint)
action (varchar)
before (json)
after (json)
actor_id (bigint)
ip (varchar)
ua (varchar)
created_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsboardsboard" [ +label=< + + + + + + + + + + + + + + +
Board
id (bigint)
tenant_id (bigint)
board_code (varchar)
name (varchar)
description (varchar)
editor_type (varchar)
allow_files (tinyint)
max_file_count (int)
max_file_size (int)
extra_settings (json)
is_active (tinyint)
created_at (timestamp)
updated_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsboardsboardcomment" [ +label=< + + + + + + + + + + + + +
BoardComment
id (bigint)
post_id (bigint)
tenant_id (bigint)
user_id (bigint)
parent_id (bigint)
content (text)
ip_address (varchar)
status (varchar)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsboardsboardsetting" [ +label=< + + + + + + + + + + + +
BoardSetting
id (bigint)
board_id (bigint)
name (varchar)
field_key (varchar)
field_type (varchar)
field_meta (json)
is_required (tinyint)
sort_order (int)
created_at (timestamp)
updated_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsboardspost" [ +label=< + + + + + + + + + + + + + + + + +
Post
id (bigint)
tenant_id (bigint)
board_id (bigint)
user_id (bigint)
title (varchar)
content (longtext)
editor_type (varchar)
ip_address (varchar)
is_notice (tinyint)
is_secret (tinyint)
views (int)
status (varchar)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsboardspostcustomfieldvalue" [ +label=< + + + + + + + +
PostCustomFieldValue
id (bigint)
post_id (bigint)
field_id (bigint)
value (text)
created_at (timestamp)
updated_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelscalculationcalculationconfig" [ +label=< + + + + + + + + + + + + + + + + +
CalculationConfig
id (bigint)
tenant_id (bigint)
company_name (varchar)
formula_type (varchar)
version (varchar)
formula_expression (text)
parameters (json)
conditions (json)
validation_rules (json)
description (text)
is_active (tinyint)
created_at (timestamp)
updated_at (timestamp)
created_by (bigint)
updated_by (bigint)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelscommonscategory" [ +label=< + + + + + + + + + + + + + + + + + +
Category
id (bigint)
tenant_id (bigint)
parent_id (bigint)
code_group (varchar)
profile_code (varchar)
code (varchar)
name (varchar)
is_active (tinyint)
sort_order (int)
description (varchar)
created_by (bigint)
updated_by (bigint)
deleted_by (bigint)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelscommonscategoryfield" [ +label=< + + + + + + + + + + + + + + + + + + +
CategoryField
id (bigint)
tenant_id (bigint)
category_id (bigint)
field_key (varchar)
field_name (varchar)
field_type (varchar)
is_required (char)
sort_order (int)
default_value (varchar)
options (text)
description (varchar)
created_by (bigint)
updated_by (bigint)
deleted_by (bigint)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelscommonscategorylog" [ +label=< + + + + + + + + + + +
CategoryLog
id (bigint)
category_id (bigint)
tenant_id (bigint)
action (varchar)
changed_by (bigint)
changed_at (timestamp)
before_json (json)
after_json (json)
remarks (varchar)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelscommonscategorytemplate" [ +label=< + + + + + + + + + + + + + +
CategoryTemplate
id (bigint)
tenant_id (bigint)
category_id (bigint)
version_no (int)
template_json (json)
applied_at (timestamp)
created_by (bigint)
updated_by (bigint)
deleted_by (bigint)
remarks (varchar)
created_at (timestamp)
updated_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelscommonsclassification" [ +label=< + + + + + + + + + + +
Classification
id (bigint)
tenant_id (bigint)
group (varchar)
code (varchar)
name (varchar)
is_active (tinyint)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelscommonsfile" [ +label=< + + + + + + + + + + + + + + + + +
File
id (bigint)
tenant_id (bigint)
file_path (varchar)
original_name (varchar)
file_name (varchar)
file_name_old (varchar)
file_size (int)
mime_type (varchar)
description (varchar)
fileable_id (bigint)
fileable_type (varchar)
uploaded_by (bigint)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelscommonsmenu" [ +label=< + + + + + + + + + + + + + + + + + + +
Menu
id (bigint)
tenant_id (bigint)
parent_id (bigint)
name (varchar)
url (varchar)
is_active (tinyint)
sort_order (int)
hidden (tinyint)
is_external (tinyint)
external_url (varchar)
icon (varchar)
created_at (timestamp)
updated_at (timestamp)
created_by (bigint)
updated_by (bigint)
deleted_by (bigint)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelscommonstag" [ +label=< + + + + + + +
Tag
id (bigint)
tenant_id (bigint)
name (varchar)
created_at (timestamp)
updated_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsdesignbomtemplate" [ +label=< + + + + + + + + + + + + + +
BomTemplate
id (bigint)
tenant_id (bigint)
model_version_id (bigint)
name (varchar)
is_primary (tinyint)
notes (text)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
calculation_schema (json)
company_type (varchar)
formula_version (varchar)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsdesignbomtemplateitem" [ +label=< + + + + + + + + + + + + + + + + + +
BomTemplateItem
id (bigint)
tenant_id (bigint)
bom_template_id (bigint)
ref_type (varchar)
ref_id (bigint)
qty (decimal)
waste_rate (decimal)
uom_id (bigint)
notes (varchar)
sort_order (int)
created_at (timestamp)
updated_at (timestamp)
is_calculated (tinyint)
calculation_formula (text)
depends_on (json)
calculation_config (json)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsdesigndesignmodel" [ +label=< + + + + + + + + + + + + +
DesignModel
id (bigint)
tenant_id (bigint)
code (varchar)
name (varchar)
category_id (bigint)
lifecycle (varchar)
description (text)
is_active (tinyint)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsdesignmodelversion" [ +label=< + + + + + + + + + + + + + +
ModelVersion
id (bigint)
tenant_id (bigint)
model_id (bigint)
version_no (int)
status (varchar)
effective_from (datetime)
effective_to (datetime)
notes (text)
is_active (tinyint)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsestimateestimate" [ +label=< + + + + + + + + + + + + + + + + + + + + + +
Estimate
id (bigint)
tenant_id (bigint)
model_set_id (bigint)
estimate_no (varchar)
estimate_name (varchar)
customer_name (varchar)
project_name (varchar)
parameters (json)
calculated_results (json)
bom_data (json)
total_amount (decimal)
status (enum)
notes (text)
valid_until (date)
created_by (bigint)
updated_by (bigint)
deleted_by (bigint)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsestimateestimateitem" [ +label=< + + + + + + + + + + + + + + + + + + + + +
EstimateItem
id (bigint)
tenant_id (bigint)
estimate_id (bigint)
sequence (int)
item_name (varchar)
item_description (text)
parameters (json)
calculated_values (json)
unit_price (decimal)
quantity (decimal)
total_price (decimal)
bom_components (json)
notes (text)
created_by (bigint)
updated_by (bigint)
deleted_by (bigint)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsestimatesmainrequestestimate" [ +label=< + +
MainRequestEstimate
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsmainrequest" [ +label=< + + + + + + + + + + +
MainRequest
id (bigint)
tenant_id (bigint)
status_code (varchar)
description (text)
created_by (bigint)
updated_by (bigint)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsmainrequestflow" [ +label=< + + + + + + + + + + + +
MainRequestFlow
id (bigint)
main_request_id (bigint)
flowable_type (varchar)
flowable_id (bigint)
status_code (varchar)
action (varchar)
content (text)
actor_id (bigint)
created_at (timestamp)
updated_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsmaterialsmaterial" [ +label=< + + + + + + + + + + + + + + + + + + + +
Material
id (bigint)
tenant_id (bigint)
category_id (bigint)
name (varchar)
item_name (varchar)
specification (varchar)
material_code (varchar)
unit (varchar)
is_inspection (char)
search_tag (text)
remarks (text)
attributes (json)
options (json)
created_by (bigint)
updated_by (bigint)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsmaterialsmaterialinspection" [ +label=< + + + + + + + + + + + + + + + + +
MaterialInspection
id (bigint)
receipt_id (bigint)
tenant_id (bigint)
inspection_date (date)
inspector_name (varchar)
approver_name (varchar)
judgment_code (varchar)
status_code (varchar)
result_file_path (text)
remarks (text)
created_by (bigint)
updated_by (bigint)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsmaterialsmaterialinspectionitem" [ +label=< + + + + + + + + + + +
MaterialInspectionItem
id (bigint)
inspection_id (bigint)
item_name (varchar)
is_checked (char)
created_by (bigint)
updated_by (bigint)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsmaterialsmaterialreceipt" [ +label=< + + + + + + + + + + + + + + + + + + + + + +
MaterialReceipt
id (bigint)
material_id (bigint)
tenant_id (bigint)
receipt_date (date)
lot_number (varchar)
received_qty (decimal)
unit (varchar)
supplier_name (varchar)
manufacturer_name (varchar)
purchase_price_excl_vat (decimal)
weight_kg (decimal)
status_code (varchar)
is_inspection (char)
inspection_date (date)
remarks (text)
created_by (bigint)
updated_by (bigint)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsmembersuser" [ +label=< + + + + + + + + + + + + + + + + + + + +
User
id (bigint)
user_id (varchar)
phone (varchar)
options (json)
name (varchar)
email (varchar)
email_verified_at (timestamp)
password (varchar)
last_login_at (timestamp)
two_factor_secret (text)
two_factor_recovery_codes (text)
two_factor_confirmed_at (timestamp)
remember_token (varchar)
current_team_id (bigint)
profile_photo_path (varchar)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsmembersusermenupermission" [ +label=< + +
UserMenuPermission
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsmembersuserrole" [ +label=< + + + + + + + + + +
UserRole
id (bigint)
user_id (bigint)
tenant_id (bigint)
role_id (bigint)
assigned_at (timestamp)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsmembersusertenant" [ +label=< + + + + + + + + + + + +
UserTenant
id (bigint)
user_id (bigint)
tenant_id (bigint)
is_active (tinyint)
is_default (tinyint)
joined_at (timestamp)
left_at (timestamp)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsordersorder" [ +label=< + + + + + + + + + + + + + + + + + + + + +
Order
id (bigint)
tenant_id (bigint)
order_no (varchar)
order_type_code (varchar)
status_code (varchar)
category_code (varchar)
product_id (bigint)
received_at (datetime)
writer_id (bigint)
client_id (bigint)
client_contact (varchar)
site_name (varchar)
quantity (int)
delivery_date (date)
delivery_method_code (varchar)
memo (text)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsordersorderhistory" [ +label=< + + + + + + + + + +
OrderHistory
id (bigint)
tenant_id (bigint)
order_id (bigint)
history_type (varchar)
content (text)
created_by (bigint)
created_at (timestamp)
updated_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsordersorderitem" [ +label=< + + + + + + + + + + + + + + +
OrderItem
id (bigint)
tenant_id (bigint)
order_id (bigint)
serial_no (int)
product_id (bigint)
quantity (int)
status_code (varchar)
design_code (varchar)
remarks (text)
attributes (json)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsordersorderitemcomponent" [ +label=< + + + + + + + + + + + + + + + +
OrderItemComponent
id (bigint)
tenant_id (bigint)
order_item_id (bigint)
component_type (varchar)
component_id (bigint)
quantity (decimal)
unit (varchar)
price (decimal)
remarks (varchar)
created_by (bigint)
updated_by (bigint)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsordersorderversion" [ +label=< + + + + + + + + + + + +
OrderVersion
id (bigint)
tenant_id (bigint)
order_id (bigint)
version_no (int)
version_type (varchar)
status_code (varchar)
changed_fields (json)
changed_by (bigint)
changed_at (timestamp)
change_note (varchar)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelspermissionspermissionoverride" [ +label=< + + + + + + + + + + + + + +
PermissionOverride
id (bigint)
tenant_id (bigint)
model_type (varchar)
model_id (bigint)
permission_id (bigint)
effect (tinyint)
reason (varchar)
effective_from (timestamp)
effective_to (timestamp)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelspermissionsrole" [ +label=< + + + + + + + + + +
Role
id (bigint)
tenant_id (bigint)
name (varchar)
guard_name (varchar)
description (varchar)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelspermissionsrolemenupermission" [ +label=< + +
RoleMenuPermission
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsproductscommoncode" [ +label=< + + + + + + + + + + + + + + +
CommonCode
id (bigint)
tenant_id (bigint)
code_group (varchar)
code (varchar)
name (varchar)
parent_id (bigint)
attributes (json)
description (varchar)
is_active (tinyint)
sort_order (int)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsproductspart" [ +label=< + +
Part
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsproductspricehistory" [ +label=< + + + + + + + + + + + + + + +
PriceHistory
id (bigint)
tenant_id (bigint)
item_type_code (varchar)
item_id (bigint)
price_type_code (varchar)
price (decimal)
started_at (date)
ended_at (date)
created_by (bigint)
updated_by (bigint)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsproductsproduct" [ +label=< + + + + + + + + + + + + + + + + + + + +
Product
id (bigint)
tenant_id (bigint)
code (varchar)
name (varchar)
unit (varchar)
category_id (bigint)
product_type (varchar)
attributes (json)
description (varchar)
is_sellable (tinyint)
is_purchasable (tinyint)
is_producible (tinyint)
is_active (tinyint)
created_by (bigint)
updated_by (bigint)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsproductsproductcomponent" [ +label=< + + + + + + + + + + + + + + + +
ProductComponent
id (bigint)
tenant_id (bigint)
parent_product_id (bigint)
category_id (bigint)
category_name (varchar)
ref_type (varchar)
ref_id (bigint)
quantity (decimal)
sort_order (int)
created_by (bigint)
updated_by (bigint)
deleted_at (timestamp)
created_at (timestamp)
updated_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsqualityslot" [ +label=< + + + + + + + + + + + + + + + + + +
Lot
id (bigint)
tenant_id (bigint)
lot_number (varchar)
material_id (bigint)
specification (varchar)
length (varchar)
quantity (int)
raw_lot_number (varchar)
fabric_lot (varchar)
author (varchar)
remarks (text)
created_by (bigint)
updated_by (bigint)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelsqualityslotsale" [ +label=< + + + + + + + + + + + + + +
LotSale
id (bigint)
tenant_id (bigint)
lot_id (bigint)
sale_date (date)
author (varchar)
workplace_name (varchar)
remarks (text)
created_by (bigint)
updated_by (bigint)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelssiteadmin" [ +label=< + +
SiteAdmin
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelstenantsdepartment" [ +label=< + + + + + + + + + + + + + + + +
Department
id (bigint)
tenant_id (bigint)
parent_id (bigint)
code (varchar)
name (varchar)
description (varchar)
is_active (tinyint)
sort_order (int)
created_by (bigint)
updated_by (bigint)
deleted_by (bigint)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelstenantspayment" [ +label=< + + + + + + + + + + + + +
Payment
id (bigint)
subscription_id (bigint)
amount (decimal)
payment_method (varchar)
transaction_id (varchar)
paid_at (datetime)
status (varchar)
memo (text)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelstenantspivotsdepartmentuser" [ +label=< + + + + + + + + + + + +
DepartmentUser
id (bigint)
tenant_id (bigint)
department_id (bigint)
user_id (bigint)
is_primary (tinyint)
joined_at (timestamp)
left_at (timestamp)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelstenantsplan" [ +label=< + + + + + + + + + + + + +
Plan
id (bigint)
name (varchar)
code (varchar)
description (text)
price (decimal)
billing_cycle (varchar)
features (json)
is_active (tinyint)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelstenantssettingfielddef" [ +label=< + + + + + + + + + + + + + + +
SettingFieldDef
id (bigint)
field_key (varchar)
label (varchar)
data_type (varchar)
input_type (varchar)
option_source (varchar)
option_payload (json)
comment (text)
created_at (timestamp)
updated_at (timestamp)
storage_area (varchar)
storage_key (varchar)
is_core (tinyint)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelstenantssubscription" [ +label=< + + + + + + + + + + +
Subscription
id (bigint)
tenant_id (bigint)
plan_id (bigint)
started_at (date)
ended_at (date)
status (varchar)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelstenantstenant" [ +label=< + + + + + + + + + + + + + + + + + + + + + + + + + + +
Tenant
id (bigint)
company_name (varchar)
code (varchar)
email (varchar)
phone (varchar)
address (varchar)
business_num (varchar)
corp_reg_no (varchar)
ceo_name (varchar)
homepage (varchar)
fax (varchar)
logo (varchar)
admin_memo (text)
options (json)
tenant_st_code (varchar)
plan_id (bigint)
subscription_id (bigint)
max_users (int)
trial_ends_at (datetime)
expires_at (datetime)
last_paid_at (datetime)
billing_tp_code (varchar)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelstenantstenantfieldsetting" [ +label=< + + + + + + + + + +
TenantFieldSetting
id (bigint)
tenant_id (bigint)
field_key (varchar)
enabled (tinyint)
required (tinyint)
sort_order (int)
option_group_id (bigint)
code_group (varchar)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelstenantstenantoptiongroup" [ +label=< + + + + + + +
TenantOptionGroup
id (bigint)
tenant_id (bigint)
group_key (varchar)
name (varchar)
description (varchar)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelstenantstenantoptionvalue" [ +label=< + + + + + + + +
TenantOptionValue
id (bigint)
group_id (bigint)
value_key (varchar)
value_label (varchar)
sort_order (int)
is_active (tinyint)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"appmodelstenantstenantuserprofile" [ +label=< + + + + + + + + + + + + + + + +
TenantUserProfile
id (bigint)
tenant_id (bigint)
user_id (bigint)
department_id (bigint)
position_key (varchar)
job_title_key (varchar)
work_location_key (varchar)
employment_type_key (varchar)
manager_user_id (bigint)
json_extra (json)
created_at (timestamp)
updated_at (timestamp)
profile_photo_path (varchar)
display_name (varchar)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"product_components" [ +label=< + + + + + + + + + + + + + + + +
Pivot
id (bigint)
tenant_id (bigint)
parent_product_id (bigint)
category_id (bigint)
category_name (varchar)
ref_type (varchar)
ref_id (bigint)
quantity (decimal)
sort_order (int)
created_by (bigint)
updated_by (bigint)
deleted_at (timestamp)
created_at (timestamp)
updated_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"department_user" [ +label=< + + + + + + + + + + + +
DepartmentUser
id (bigint)
tenant_id (bigint)
department_id (bigint)
user_id (bigint)
is_primary (tinyint)
joined_at (timestamp)
left_at (timestamp)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +"user_tenants" [ +label=< + + + + + + + + + + + +
Pivot
id (bigint)
user_id (bigint)
tenant_id (bigint)
is_active (tinyint)
is_default (tinyint)
joined_at (timestamp)
left_at (timestamp)
created_at (timestamp)
updated_at (timestamp)
deleted_at (timestamp)
> +margin="0" +shape="rectangle" +fontname="Helvetica Neue" +] +} \ No newline at end of file diff --git a/routes/api.php b/routes/api.php index 0832592..6b33635 100644 --- a/routes/api.php +++ b/routes/api.php @@ -26,6 +26,7 @@ use App\Http\Controllers\Api\V1\CategoryFieldController; use App\Http\Controllers\Api\V1\CategoryTemplateController; use App\Http\Controllers\Api\V1\ClassificationController; +use App\Http\Controllers\Api\V1\ClientController; // 설계 전용 (디자인 네임스페이스) @@ -281,12 +282,29 @@ Route::delete('/{id}', [ClassificationController::class, 'destroy'])->whereNumber('id')->name('v1.classifications.destroy'); // 삭제 }); + // Clients (거래처 관리) + Route::prefix('clients')->group(function () { + Route::get ('', [ClientController::class, 'index'])->name('v1.clients.index'); // 목록 + Route::post ('', [ClientController::class, 'store'])->name('v1.clients.store'); // 생성 + Route::get ('/{id}', [ClientController::class, 'show'])->whereNumber('id')->name('v1.clients.show'); // 단건 + Route::put ('/{id}', [ClientController::class, 'update'])->whereNumber('id')->name('v1.clients.update'); // 수정 + Route::delete('/{id}', [ClientController::class, 'destroy'])->whereNumber('id')->name('v1.clients.destroy'); // 삭제 + Route::patch ('/{id}/toggle', [ClientController::class, 'toggle'])->whereNumber('id')->name('v1.clients.toggle'); // 활성/비활성 + }); + // Products & Materials (제품/자재 통합 관리) Route::prefix('products')->group(function (){ // 제품 카테고리 (기존 product/category에서 이동) Route::get ('/categories', [ProductController::class, 'getCategory'])->name('v1.products.categories'); // 제품 카테고리 + // 자재 관리 (기존 독립 materials에서 이동) - ProductController 기본 라우팅보다 앞에 위치 + Route::get ('/materials', [MaterialController::class, 'index'])->name('v1.products.materials.index'); // 자재 목록 + Route::post ('/materials', [MaterialController::class, 'store'])->name('v1.products.materials.store'); // 자재 생성 + Route::get ('/materials/{id}', [MaterialController::class, 'show'])->name('v1.products.materials.show'); // 자재 단건 + Route::patch ('/materials/{id}', [MaterialController::class, 'update'])->name('v1.products.materials.update'); // 자재 수정 + Route::delete('/materials/{id}', [MaterialController::class, 'destroy'])->name('v1.products.materials.destroy'); // 자재 삭제 + // (선택) 드롭다운/모달용 간편 검색 & 활성 토글 Route::get ('/search', [ProductController::class, 'search'])->name('v1.products.search'); Route::post ('/{id}/toggle', [ProductController::class, 'toggle'])->name('v1.products.toggle'); @@ -300,13 +318,6 @@ // BOM 카테고리 Route::get('bom/categories', [ProductBomItemController::class, 'suggestCategories'])->name('v1.products.bom.categories.suggest'); // 전역(테넌트) 추천 Route::get('{id}/bom/categories', [ProductBomItemController::class, 'listCategories'])->name('v1.products.bom.categories'); // 해당 제품에서 사용 중 - - // 자재 관리 (기존 독립 materials에서 이동) - Route::get ('/materials', [MaterialController::class, 'index'])->name('v1.products.materials.index'); // 자재 목록 - Route::post ('/materials', [MaterialController::class, 'store'])->name('v1.products.materials.store'); // 자재 생성 - Route::get ('/materials/{id}', [MaterialController::class, 'show'])->name('v1.products.materials.show'); // 자재 단건 - Route::patch ('/materials/{id}', [MaterialController::class, 'update'])->name('v1.products.materials.update'); // 자재 수정 - Route::delete('/materials/{id}', [MaterialController::class, 'destroy'])->name('v1.products.materials.destroy'); // 자재 삭제 }); // BOM (product_components: ref_type=PRODUCT|MATERIAL)