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=<>
+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=<>
+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=<>
+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=<>
+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=<>
+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)