feat: [material] 부적합관리 API Phase 1-A 구현

- Migration: nonconforming_reports, nonconforming_report_items 테이블
- Model: NonconformingReport, NonconformingReportItem (관계, cast, scope)
- FormRequest: Store/Update 검증 (items 배열 포함)
- Service: CRUD + 채번(NC-YYYYMMDD-NNN) + 비용 자동 계산 + 상태 전이
- Controller: REST 7개 엔드포인트 (목록/통계/상세/등록/수정/삭제/상태변경)
- Route: /api/v1/material/nonconforming-reports
- i18n: 부적합관리 에러 메시지 (ko)
This commit is contained in:
김보곤
2026-03-19 08:36:45 +09:00
parent d8a57f71c6
commit 847c60b03d
9 changed files with 826 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('nonconforming_reports', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->string('nc_number', 30)->comment('부적합번호 NC-YYYYMMDD-NNN');
$table->string('status', 20)->default('RECEIVED')->comment('상태: RECEIVED/ANALYZING/RESOLVED/CLOSED');
$table->string('nc_type', 20)->comment('부적합유형: material/process/construction/other');
$table->date('occurred_at')->comment('발생일');
$table->date('confirmed_at')->nullable()->comment('불량확인일');
$table->string('site_name', 100)->nullable()->comment('현장명');
$table->unsignedBigInteger('department_id')->nullable()->comment('부서');
$table->unsignedBigInteger('order_id')->nullable()->comment('관련 수주');
$table->unsignedBigInteger('item_id')->nullable()->comment('관련 품목');
$table->decimal('defect_quantity', 10, 2)->nullable()->comment('불량 수량');
$table->string('unit', 20)->nullable()->comment('단위');
$table->text('defect_description')->nullable()->comment('불량 상세 설명');
$table->text('cause_analysis')->nullable()->comment('원인 분석');
$table->text('corrective_action')->nullable()->comment('처리 방안 및 개선 사항');
$table->date('action_completed_at')->nullable()->comment('조치 완료일');
$table->unsignedBigInteger('action_manager_id')->nullable()->comment('조치 담당자');
$table->unsignedBigInteger('related_employee_id')->nullable()->comment('관련 직원');
$table->decimal('material_cost', 12, 0)->default(0)->comment('자재 비용');
$table->decimal('shipping_cost', 12, 0)->default(0)->comment('운송 비용');
$table->decimal('construction_cost', 12, 0)->default(0)->comment('시공 비용');
$table->decimal('other_cost', 12, 0)->default(0)->comment('기타 비용');
$table->decimal('total_cost', 12, 0)->default(0)->comment('비용 합계');
$table->text('remarks')->nullable()->comment('비고');
$table->string('drawing_location', 255)->nullable()->comment('도면 저장 위치');
$table->json('options')->nullable()->comment('확장 속성');
$table->unsignedBigInteger('created_by')->default(0)->comment('등록자');
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자');
$table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자');
$table->timestamps();
$table->softDeletes();
$table->index(['tenant_id', 'status']);
$table->index(['tenant_id', 'nc_type']);
$table->index(['tenant_id', 'occurred_at']);
$table->unique(['tenant_id', 'nc_number']);
});
Schema::create('nonconforming_report_items', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
$table->unsignedBigInteger('nonconforming_report_id')->comment('부적합 보고서 ID');
$table->unsignedBigInteger('item_id')->nullable()->comment('품목 마스터 연결');
$table->string('item_name', 100)->comment('품목명');
$table->string('specification', 100)->nullable()->comment('규격/사양');
$table->decimal('quantity', 10, 2)->default(0)->comment('수량');
$table->decimal('unit_price', 12, 0)->default(0)->comment('단가');
$table->decimal('amount', 12, 0)->default(0)->comment('금액');
$table->integer('sort_order')->default(0)->comment('정렬 순서');
$table->string('remarks', 255)->nullable()->comment('비고');
$table->json('options')->nullable()->comment('확장 속성');
$table->timestamps();
$table->index('nonconforming_report_id');
$table->foreign('nonconforming_report_id')
->references('id')
->on('nonconforming_reports')
->onDelete('cascade');
});
}
public function down(): void
{
Schema::dropIfExists('nonconforming_report_items');
Schema::dropIfExists('nonconforming_reports');
}
};