refactor: 검사 테이블 통합 및 출하 대시보드 수정

## 변경 사항

### 검사 시스템 통합
- 레거시 검사 모델 삭제 (MaterialInspection, MaterialInspectionItem, MaterialReceipt)
- 통합 검사 모델 추가 (Inspection.php) - IQC/PQC/FQC 지원
- 품목 입고 모델 추가 (ItemReceipt.php)
- PricingService에서 ItemReceipt 참조로 변경

### 출하 대시보드 수정
- ShipmentService stats() 프론트엔드 호환 필드 추가
  - today_shipment_count, scheduled_count, shipping_count, urgent_count

### 마이그레이션
- inspections 테이블 생성 (IQC/PQC/FQC 통합)
- item_receipts로 테이블명 변경

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-29 19:31:51 +09:00
parent 54ebd5e0fd
commit 39538aa812
9 changed files with 483 additions and 103 deletions

View File

@@ -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`

View File

@@ -0,0 +1,113 @@
<?php
namespace App\Models\Items;
use App\Models\Scopes\BelongsToTenant;
use App\Models\Tenants\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* 품목 입고 모델
*
* @property int $id
* @property int|null $item_id 품목 ID
* @property int $tenant_id 테넌트 ID
* @property string $receipt_date 입고일
* @property string $lot_number LOT번호
* @property float $received_qty 입고수량
* @property string $unit 단위
* @property string|null $supplier_name 공급업체명
* @property string|null $manufacturer_name 제조업체명
* @property float|null $purchase_price_excl_vat 매입단가(부가세 제외)
* @property float|null $weight_kg 중량(kg)
* @property string $status_code 상태코드
* @property string $is_inspection 검사여부 (Y/N)
* @property string|null $inspection_date 검사일
* @property string|null $remarks 비고
*
* @mixin IdeHelperItemReceipt
*/
class ItemReceipt extends Model
{
use SoftDeletes;
protected $table = 'item_receipts';
protected $fillable = [
'item_id',
'tenant_id',
'receipt_date',
'lot_number',
'received_qty',
'unit',
'supplier_name',
'manufacturer_name',
'purchase_price_excl_vat',
'weight_kg',
'status_code',
'is_inspection',
'inspection_date',
'remarks',
'created_by',
'updated_by',
];
protected $casts = [
'receipt_date' => '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);
}
}

View File

@@ -1,26 +0,0 @@
<?php
namespace App\Models\Materials;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* @mixin IdeHelperMaterialInspection
*/
class MaterialInspection extends Model
{
use SoftDeletes;
// 입고 내역
public function receipt()
{
return $this->belongsTo(MaterialReceipt::class, 'receipt_id');
}
// 검사 항목
public function items()
{
return $this->hasMany(MaterialInspectionItem::class, 'inspection_id');
}
}

View File

@@ -1,20 +0,0 @@
<?php
namespace App\Models\Materials;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* @mixin IdeHelperMaterialInspectionItem
*/
class MaterialInspectionItem extends Model
{
use SoftDeletes;
// 검사 내역
public function inspection()
{
return $this->belongsTo(MaterialInspection::class, 'inspection_id');
}
}

View File

@@ -1,33 +0,0 @@
<?php
namespace App\Models\Materials;
use App\Models\Items\Item;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* @mixin IdeHelperMaterialReceipt
*/
class MaterialReceipt extends Model
{
use SoftDeletes;
protected $fillable = [
'item_id', 'receipt_date', 'lot_number', 'received_qty', 'unit',
'supplier_name', 'manufacturer_name', 'purchase_price_excl_vat',
'weight_kg', 'status_code', 'is_inspection', 'inspection_date', 'remarks',
];
// 품목 (통합 items 테이블)
public function item()
{
return $this->belongsTo(Item::class, 'item_id');
}
// 수입검사 내역
public function inspections()
{
return $this->hasMany(MaterialInspection::class, 'receipt_id');
}
}

View File

@@ -0,0 +1,221 @@
<?php
namespace App\Models\Qualitys;
use App\Models\Items\Item;
use App\Models\Scopes\BelongsToTenant;
use App\Models\Tenants\User;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* 통합 검사 모델 (IQC/PQC/FQC)
*
* @property int $id
* @property int $tenant_id
* @property string $inspection_no 검사번호
* @property string $inspection_type 검사유형 (IQC, PQC, FQC)
* @property string $status 상태 (waiting, in_progress, completed)
* @property string|null $result 판정결과 (pass, fail)
* @property string $request_date 검사요청일
* @property string|null $inspection_date 검사일
* @property int|null $item_id 품목 ID
* @property string $lot_no LOT번호
* @property int|null $inspector_id 검사자 ID
* @property array|null $meta 메타정보 (process_name, quantity, unit 등)
* @property array|null $items 검사항목 배열
* @property array|null $attachments 첨부파일 배열
* @property array|null $extra 추가정보 (remarks, opinion 등)
*
* @mixin IdeHelperInspection
*/
class Inspection extends Model
{
use SoftDeletes;
protected $table = 'inspections';
protected $fillable = [
'tenant_id',
'inspection_no',
'inspection_type',
'status',
'result',
'request_date',
'inspection_date',
'item_id',
'lot_no',
'inspector_id',
'meta',
'items',
'attachments',
'extra',
'created_by',
'updated_by',
];
protected $casts = [
'meta' => '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);
}
}

View File

@@ -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();

View File

@@ -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,
];
}

View File

@@ -0,0 +1,116 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* 통합 검사 테이블 생성 및 레거시 테이블 정리
* - inspections: IQC/PQC/FQC 통합 관리
* - material_receipts → item_receipts 이름 변경
* - 삭제: material_inspections, material_inspection_items
*/
public function up(): void
{
// 1. 통합 검사 테이블 생성
Schema::create('inspections', function (Blueprint $table) {
$table->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');
});
}
};