diff --git a/LOGICAL_RELATIONSHIPS.md b/LOGICAL_RELATIONSHIPS.md index 9c95aa1..97ac244 100644 --- a/LOGICAL_RELATIONSHIPS.md +++ b/LOGICAL_RELATIONSHIPS.md @@ -1,6 +1,6 @@ # 논리적 데이터베이스 관계 문서 -> **자동 생성**: 2025-12-26 18:32:26 +> **자동 생성**: 2025-12-29 18:06:50 > **소스**: Eloquent 모델 관계 분석 ## 📊 모델별 관계 현황 @@ -51,8 +51,8 @@ ### posts **모델**: `App\Models\Boards\Post` - **board()**: belongsTo → `boards` +- **files()**: hasMany → `files` - **comments()**: hasMany → `board_comments` -- **files()**: morphMany → `files` ### post_custom_field_values **모델**: `App\Models\Boards\PostCustomFieldValue` @@ -206,6 +206,11 @@ ### item_details - **item()**: belongsTo → `items` +### item_receipts +**모델**: `App\Models\Items\ItemReceipt` + +- **item()**: belongsTo → `items` + ### login_tokens **모델**: `App\Models\LoginToken` @@ -222,23 +227,6 @@ ### main_request_flows - **mainRequest()**: belongsTo → `main_requests` - **flowable()**: morphTo → `(Polymorphic)` -### material_inspections -**모델**: `App\Models\Materials\MaterialInspection` - -- **receipt()**: belongsTo → `material_receipts` -- **items()**: hasMany → `material_inspection_items` - -### material_inspection_items -**모델**: `App\Models\Materials\MaterialInspectionItem` - -- **inspection()**: belongsTo → `material_inspections` - -### material_receipts -**모델**: `App\Models\Materials\MaterialReceipt` - -- **item()**: belongsTo → `items` -- **inspections()**: hasMany → `material_inspections` - ### users **모델**: `App\Models\Members\User` @@ -361,6 +349,16 @@ ### popups - **creator()**: belongsTo → `users` - **updater()**: belongsTo → `users` +### process +**모델**: `App\Models\Process` + +- **classificationRules()**: hasMany → `process_classification_rules` + +### process_classification_rules +**모델**: `App\Models\ProcessClassificationRule` + +- **process()**: belongsTo → `processes` + ### work_orders **모델**: `App\Models\Production\WorkOrder` @@ -419,6 +417,11 @@ ### push_notification_settings **모델**: `App\Models\PushNotificationSetting` +### inspections +**모델**: `App\Models\Qualitys\Inspection` + +- **item()**: belongsTo → `items` + ### lots **모델**: `App\Models\Qualitys\Lot` @@ -701,6 +704,7 @@ ### sites ### stocks **모델**: `App\Models\Tenants\Stock` +- **item()**: belongsTo → `items` - **creator()**: belongsTo → `users` - **lots()**: hasMany → `stock_lots` diff --git a/app/Models/Items/ItemReceipt.php b/app/Models/Items/ItemReceipt.php new file mode 100644 index 0000000..87d46d2 --- /dev/null +++ b/app/Models/Items/ItemReceipt.php @@ -0,0 +1,113 @@ + 'date', + 'inspection_date' => 'date', + 'received_qty' => 'decimal:2', + 'purchase_price_excl_vat' => 'decimal:2', + 'weight_kg' => 'decimal:2', + ]; + + protected static function booted(): void + { + static::addGlobalScope(new BelongsToTenant); + } + + // ===== Relationships ===== + + /** + * 품목 + */ + public function item() + { + return $this->belongsTo(Item::class, 'item_id'); + } + + /** + * 생성자 + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + // ===== Scopes ===== + + /** + * 특정 품목의 입고 내역 + */ + public function scopeForItem($query, int $itemId) + { + return $query->where('item_id', $itemId); + } + + /** + * 특정 일자 이전 입고 + */ + public function scopeBeforeDate($query, string $date) + { + return $query->where('receipt_date', '<=', $date); + } + + /** + * 단가가 있는 입고만 + */ + public function scopeWithPrice($query) + { + return $query->whereNotNull('purchase_price_excl_vat') + ->where('purchase_price_excl_vat', '>', 0); + } +} diff --git a/app/Models/Materials/MaterialInspection.php b/app/Models/Materials/MaterialInspection.php deleted file mode 100644 index b9d6b38..0000000 --- a/app/Models/Materials/MaterialInspection.php +++ /dev/null @@ -1,26 +0,0 @@ -belongsTo(MaterialReceipt::class, 'receipt_id'); - } - - // 검사 항목 - public function items() - { - return $this->hasMany(MaterialInspectionItem::class, 'inspection_id'); - } -} diff --git a/app/Models/Materials/MaterialInspectionItem.php b/app/Models/Materials/MaterialInspectionItem.php deleted file mode 100644 index b70c4b2..0000000 --- a/app/Models/Materials/MaterialInspectionItem.php +++ /dev/null @@ -1,20 +0,0 @@ -belongsTo(MaterialInspection::class, 'inspection_id'); - } -} diff --git a/app/Models/Materials/MaterialReceipt.php b/app/Models/Materials/MaterialReceipt.php deleted file mode 100644 index d021108..0000000 --- a/app/Models/Materials/MaterialReceipt.php +++ /dev/null @@ -1,33 +0,0 @@ -belongsTo(Item::class, 'item_id'); - } - - // 수입검사 내역 - public function inspections() - { - return $this->hasMany(MaterialInspection::class, 'receipt_id'); - } -} diff --git a/app/Models/Qualitys/Inspection.php b/app/Models/Qualitys/Inspection.php new file mode 100644 index 0000000..dda9494 --- /dev/null +++ b/app/Models/Qualitys/Inspection.php @@ -0,0 +1,221 @@ + 'array', + 'items' => 'array', + 'attachments' => 'array', + 'extra' => 'array', + 'request_date' => 'date', + 'inspection_date' => 'date', + ]; + + /** + * 검사 유형 상수 + */ + public const TYPE_IQC = 'IQC'; // 수입검사 (Incoming Quality Control) + + public const TYPE_PQC = 'PQC'; // 공정검사 (Process Quality Control) + + public const TYPE_FQC = 'FQC'; // 최종검사 (Final Quality Control) + + /** + * 상태 상수 + */ + public const STATUS_WAITING = 'waiting'; + + public const STATUS_IN_PROGRESS = 'in_progress'; + + public const STATUS_COMPLETED = 'completed'; + + /** + * 판정결과 상수 + */ + public const RESULT_PASS = 'pass'; + + public const RESULT_FAIL = 'fail'; + + protected static function booted(): void + { + static::addGlobalScope(new BelongsToTenant); + } + + // ===== Relationships ===== + + /** + * 품목 + */ + public function item() + { + return $this->belongsTo(Item::class, 'item_id'); + } + + /** + * 검사자 + */ + public function inspector() + { + return $this->belongsTo(User::class, 'inspector_id'); + } + + /** + * 생성자 + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + // ===== Accessors (meta JSON 필드) ===== + + protected function processName(): Attribute + { + return Attribute::make( + get: fn () => $this->meta['process_name'] ?? null, + set: fn ($value) => $this->setMetaValue('process_name', $value), + ); + } + + protected function quantity(): Attribute + { + return Attribute::make( + get: fn () => $this->meta['quantity'] ?? null, + set: fn ($value) => $this->setMetaValue('quantity', $value), + ); + } + + protected function unit(): Attribute + { + return Attribute::make( + get: fn () => $this->meta['unit'] ?? null, + set: fn ($value) => $this->setMetaValue('unit', $value), + ); + } + + // ===== Accessors (extra JSON 필드) ===== + + protected function remarks(): Attribute + { + return Attribute::make( + get: fn () => $this->extra['remarks'] ?? null, + set: fn ($value) => $this->setExtraValue('remarks', $value), + ); + } + + protected function opinion(): Attribute + { + return Attribute::make( + get: fn () => $this->extra['opinion'] ?? null, + set: fn ($value) => $this->setExtraValue('opinion', $value), + ); + } + + // ===== Helper Methods ===== + + /** + * meta JSON 필드에 값 설정 + */ + protected function setMetaValue(string $key, $value): array + { + $meta = $this->meta ?? []; + $meta[$key] = $value; + $this->attributes['meta'] = json_encode($meta); + + return $meta; + } + + /** + * extra JSON 필드에 값 설정 + */ + protected function setExtraValue(string $key, $value): array + { + $extra = $this->extra ?? []; + $extra[$key] = $value; + $this->attributes['extra'] = json_encode($extra); + + return $extra; + } + + /** + * 검사번호 자동 생성 + */ + public static function generateInspectionNo(int $tenantId, string $type): string + { + $prefix = match ($type) { + self::TYPE_IQC => 'IQC', + self::TYPE_PQC => 'PQC', + self::TYPE_FQC => 'FQC', + default => 'INS', + }; + + $date = now()->format('Ymd'); + + $lastNo = static::withoutGlobalScopes() + ->where('tenant_id', $tenantId) + ->where('inspection_no', 'like', "{$prefix}-{$date}-%") + ->orderByDesc('inspection_no') + ->value('inspection_no'); + + if ($lastNo) { + $seq = (int) substr($lastNo, -4) + 1; + } else { + $seq = 1; + } + + return sprintf('%s-%s-%04d', $prefix, $date, $seq); + } +} diff --git a/app/Services/PricingService.php b/app/Services/PricingService.php index eebefc9..803f5d8 100644 --- a/app/Services/PricingService.php +++ b/app/Services/PricingService.php @@ -2,7 +2,7 @@ namespace App\Services; -use App\Models\Materials\MaterialReceipt; +use App\Models\Items\ItemReceipt; use App\Models\Products\Price; use App\Models\Products\PriceRevision; use Illuminate\Contracts\Pagination\LengthAwarePaginator; @@ -394,10 +394,10 @@ public function getCost(array $params): array ->exists(); if ($isMaterial) { - $receipt = MaterialReceipt::query() - ->where('material_id', $itemId) - ->where('receipt_date', '<=', $date) - ->whereNotNull('purchase_price_excl_vat') + $receipt = ItemReceipt::query() + ->forItem($itemId) + ->beforeDate($date) + ->withPrice() ->orderByDesc('receipt_date') ->orderByDesc('id') ->first(); diff --git a/app/Services/ShipmentService.php b/app/Services/ShipmentService.php index b9e2354..65b3a52 100644 --- a/app/Services/ShipmentService.php +++ b/app/Services/ShipmentService.php @@ -117,6 +117,11 @@ public function stats(): array 'completed' => $completed, 'urgent' => $urgent, 'today_scheduled' => $todayScheduled, + // 프론트엔드 호환 필드 (snake_case) + 'today_shipment_count' => $todayScheduled, + 'scheduled_count' => $scheduled, + 'shipping_count' => $shipping, + 'urgent_count' => $urgent, ]; } diff --git a/database/migrations/2025_12_29_175820_create_inspections_table_and_drop_legacy_material_tables.php b/database/migrations/2025_12_29_175820_create_inspections_table_and_drop_legacy_material_tables.php new file mode 100644 index 0000000..42878de --- /dev/null +++ b/database/migrations/2025_12_29_175820_create_inspections_table_and_drop_legacy_material_tables.php @@ -0,0 +1,116 @@ +id(); + $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); + + // 인덱스 컬럼 (검색/필터용) + $table->string('inspection_no', 30)->comment('검사번호'); + $table->enum('inspection_type', ['IQC', 'PQC', 'FQC'])->comment('검사유형: IQC(수입검사), PQC(공정검사), FQC(최종검사)'); + $table->enum('status', ['waiting', 'in_progress', 'completed'])->default('waiting')->comment('상태'); + $table->enum('result', ['pass', 'fail'])->nullable()->comment('판정결과'); + $table->date('request_date')->comment('검사요청일'); + $table->date('inspection_date')->nullable()->comment('검사일'); + $table->unsignedBigInteger('item_id')->nullable()->comment('품목 ID (items FK)'); + $table->string('lot_no', 50)->comment('LOT번호'); + $table->unsignedBigInteger('inspector_id')->nullable()->comment('검사자 ID (users FK)'); + + // JSON 컬럼 (비검색 데이터) + $table->json('meta')->nullable()->comment('메타정보: process_name, quantity, unit, supplier_name, manufacturer_name 등'); + $table->json('items')->nullable()->comment('검사항목 배열: [{name, type, spec, unit, result, measured_value, judgment}]'); + $table->json('attachments')->nullable()->comment('첨부파일: [{id, file_name, file_url, file_type, uploaded_at}]'); + $table->json('extra')->nullable()->comment('추가정보: remarks, opinion, approver_name 등'); + + // 감사 컬럼 + $table->unsignedBigInteger('created_by')->comment('생성자'); + $table->unsignedBigInteger('updated_by')->nullable()->comment('수정자'); + $table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자'); + $table->timestamps(); + $table->softDeletes(); + + // 인덱스 + $table->unique(['tenant_id', 'inspection_no'], 'inspections_tenant_no_unique'); + $table->index('tenant_id', 'inspections_tenant_idx'); + $table->index('inspection_type', 'inspections_type_idx'); + $table->index('status', 'inspections_status_idx'); + $table->index('result', 'inspections_result_idx'); + $table->index('request_date', 'inspections_request_date_idx'); + $table->index('inspection_date', 'inspections_inspection_date_idx'); + $table->index('item_id', 'inspections_item_idx'); + $table->index('lot_no', 'inspections_lot_idx'); + $table->index('inspector_id', 'inspections_inspector_idx'); + $table->index(['tenant_id', 'inspection_type', 'status'], 'inspections_tenant_type_status_idx'); + }); + + // 2. material_receipts → item_receipts 이름 변경 + Schema::rename('material_receipts', 'item_receipts'); + + // 3. 레거시 검사 테이블 삭제 + Schema::dropIfExists('material_inspection_items'); + Schema::dropIfExists('material_inspections'); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // 1. 통합 테이블 삭제 + Schema::dropIfExists('inspections'); + + // 2. item_receipts → material_receipts 이름 복원 + Schema::rename('item_receipts', 'material_receipts'); + + // 3. 레거시 검사 테이블 복원 + Schema::create('material_inspections', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('receipt_id'); + $table->unsignedBigInteger('tenant_id'); + $table->date('inspection_date'); + $table->string('inspector_name', 50)->nullable(); + $table->string('approver_name', 50)->nullable(); + $table->string('judgment_code', 30); + $table->string('status_code', 30); + $table->text('result_file_path')->nullable(); + $table->text('remarks')->nullable(); + $table->unsignedBigInteger('created_by'); + $table->unsignedBigInteger('updated_by')->nullable(); + $table->timestamps(); + $table->softDeletes(); + $table->unsignedBigInteger('deleted_by')->nullable(); + + $table->index('receipt_id'); + }); + + Schema::create('material_inspection_items', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('inspection_id'); + $table->string('item_name', 100); + $table->char('is_checked', 1)->default('N'); + $table->unsignedBigInteger('created_by'); + $table->unsignedBigInteger('updated_by')->nullable(); + $table->timestamps(); + $table->softDeletes(); + $table->unsignedBigInteger('deleted_by')->nullable(); + + $table->index('inspection_id'); + }); + } +}; \ No newline at end of file