feat: [생산관리] 중간검사 데이터 저장/조회 API 구현

- POST /work-orders/{id}/items/{itemId}/inspection: 품목별 검사 데이터 저장
- GET /work-orders/{id}/inspection-data: 전체 품목 검사 데이터 조회
- GET /work-orders/{id}/inspection-report: 검사 성적서용 데이터 조회
- WorkOrderItem 모델에 getInspectionData/setInspectionData 헬퍼 추가
- StoreItemInspectionRequest FormRequest 생성
- work_order_items.options['inspection_data']에 검사 결과 저장

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-09 09:00:57 +09:00
parent 78851ec04a
commit ee6794be1a
6 changed files with 253 additions and 0 deletions

View File

@@ -4,6 +4,7 @@
use App\Helpers\ApiResponse; use App\Helpers\ApiResponse;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\WorkOrder\StoreItemInspectionRequest;
use App\Http\Requests\WorkOrder\WorkOrderAssignRequest; use App\Http\Requests\WorkOrder\WorkOrderAssignRequest;
use App\Http\Requests\WorkOrder\WorkOrderIssueRequest; use App\Http\Requests\WorkOrder\WorkOrderIssueRequest;
use App\Http\Requests\WorkOrder\WorkOrderStatusRequest; use App\Http\Requests\WorkOrder\WorkOrderStatusRequest;
@@ -187,4 +188,34 @@ public function materialInputHistory(int $id)
return $this->service->getMaterialInputHistory($id); return $this->service->getMaterialInputHistory($id);
}, __('message.work_order.fetched')); }, __('message.work_order.fetched'));
} }
/**
* 품목별 중간검사 데이터 저장
*/
public function storeItemInspection(StoreItemInspectionRequest $request, int $workOrderId, int $itemId)
{
return ApiResponse::handle(function () use ($request, $workOrderId, $itemId) {
return $this->service->storeItemInspection($workOrderId, $itemId, $request->validated());
}, __('message.work_order.inspection_saved'));
}
/**
* 작업지시 전체 품목 검사 데이터 조회
*/
public function inspectionData(Request $request, int $id)
{
return ApiResponse::handle(function () use ($request, $id) {
return $this->service->getInspectionData($id, $request->all());
}, __('message.work_order.fetched'));
}
/**
* 작업지시 검사 성적서용 데이터 조회
*/
public function inspectionReport(int $id)
{
return ApiResponse::handle(function () use ($id) {
return $this->service->getInspectionReport($id);
}, __('message.work_order.fetched'));
}
} }

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Http\Requests\WorkOrder;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreItemInspectionRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'process_type' => ['required', 'string', Rule::in([
'screen', 'slat', 'slat_jointbar', 'bending', 'bending_wip',
])],
'inspection_data' => 'required|array',
'inspection_data.productName' => 'nullable|string|max:200',
'inspection_data.specification' => 'nullable|string|max:200',
'inspection_data.bendingStatus' => ['nullable', Rule::in(['good', 'bad'])],
'inspection_data.processingStatus' => ['nullable', Rule::in(['good', 'bad'])],
'inspection_data.sewingStatus' => ['nullable', Rule::in(['good', 'bad'])],
'inspection_data.assemblyStatus' => ['nullable', Rule::in(['good', 'bad'])],
'inspection_data.length' => 'nullable|numeric',
'inspection_data.width' => 'nullable|numeric',
'inspection_data.height1' => 'nullable|numeric',
'inspection_data.height2' => 'nullable|numeric',
'inspection_data.length3' => 'nullable|numeric',
'inspection_data.gap4' => 'nullable|numeric',
'inspection_data.gapStatus' => ['nullable', Rule::in(['ok', 'ng'])],
'inspection_data.gapPoints' => 'nullable|array',
'inspection_data.gapPoints.*.left' => 'nullable|numeric',
'inspection_data.gapPoints.*.right' => 'nullable|numeric',
'inspection_data.judgment' => ['nullable', Rule::in(['pass', 'fail'])],
'inspection_data.nonConformingContent' => 'nullable|string|max:1000',
];
}
public function messages(): array
{
return [
'process_type.required' => __('validation.required', ['attribute' => '공정 유형']),
'process_type.in' => __('validation.in', ['attribute' => '공정 유형']),
'inspection_data.required' => __('validation.required', ['attribute' => '검사 데이터']),
];
}
}

View File

@@ -113,6 +113,24 @@ public function scopeHasResult($query)
// 헬퍼 메서드 // 헬퍼 메서드
// ────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────
/**
* 중간검사 데이터 가져오기
*/
public function getInspectionData(): ?array
{
return $this->options['inspection_data'] ?? null;
}
/**
* 중간검사 데이터 설정
*/
public function setInspectionData(array $data): void
{
$options = $this->options ?? [];
$options['inspection_data'] = $data;
$this->options = $options;
}
/** /**
* 작업 결과 데이터 가져오기 * 작업 결과 데이터 가져오기
*/ */

View File

@@ -1519,4 +1519,151 @@ public function getMaterialInputHistory(int $workOrderId): array
]; ];
})->toArray(); })->toArray();
} }
// ──────────────────────────────────────────────────────────────
// 중간검사 관련
// ──────────────────────────────────────────────────────────────
/**
* 품목별 중간검사 데이터 저장
*/
public function storeItemInspection(int $workOrderId, int $itemId, array $data): array
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$workOrder = WorkOrder::where('tenant_id', $tenantId)->find($workOrderId);
if (! $workOrder) {
throw new NotFoundHttpException(__('error.not_found'));
}
$item = $workOrder->items()->find($itemId);
if (! $item) {
throw new NotFoundHttpException(__('error.not_found'));
}
$beforeData = $item->getInspectionData();
$inspectionData = $data['inspection_data'];
$inspectionData['process_type'] = $data['process_type'];
$inspectionData['inspected_at'] = now()->toDateTimeString();
$inspectionData['inspected_by'] = $userId;
$item->setInspectionData($inspectionData);
$item->save();
// 감사 로그
$this->auditLogger->log(
$tenantId,
self::AUDIT_TARGET,
$workOrderId,
'item_inspection_saved',
['item_id' => $itemId, 'inspection_data' => $beforeData],
['item_id' => $itemId, 'inspection_data' => $inspectionData]
);
return [
'item_id' => $item->id,
'inspection_data' => $item->getInspectionData(),
];
}
/**
* 작업지시의 전체 품목 검사 데이터 조회
*/
public function getInspectionData(int $workOrderId, array $params = []): array
{
$tenantId = $this->tenantId();
$workOrder = WorkOrder::where('tenant_id', $tenantId)->find($workOrderId);
if (! $workOrder) {
throw new NotFoundHttpException(__('error.not_found'));
}
$query = $workOrder->items()->ordered();
// 공정 유형 필터
if (! empty($params['process_type'])) {
$query->where('options->inspection_data->process_type', $params['process_type']);
}
$items = $query->get();
$inspectionMap = [];
foreach ($items as $item) {
$inspectionData = $item->getInspectionData();
if ($inspectionData) {
$inspectionMap[$item->id] = [
'item_id' => $item->id,
'item_name' => $item->item_name,
'specification' => $item->specification,
'quantity' => $item->quantity,
'sort_order' => $item->sort_order,
'options' => $item->options,
'inspection_data' => $inspectionData,
];
}
}
return [
'work_order_id' => $workOrderId,
'items' => array_values($inspectionMap),
'total' => count($inspectionMap),
];
}
/**
* 작업지시 검사 성적서용 데이터 조회 (전체 품목 + 검사 데이터 + 주문 정보)
*/
public function getInspectionReport(int $workOrderId): array
{
$tenantId = $this->tenantId();
$workOrder = WorkOrder::where('tenant_id', $tenantId)
->with(['order', 'items' => function ($q) {
$q->ordered();
}])
->find($workOrderId);
if (! $workOrder) {
throw new NotFoundHttpException(__('error.not_found'));
}
$items = $workOrder->items->map(function ($item) {
return [
'id' => $item->id,
'item_name' => $item->item_name,
'specification' => $item->specification,
'quantity' => $item->quantity,
'sort_order' => $item->sort_order,
'status' => $item->status,
'options' => $item->options,
'inspection_data' => $item->getInspectionData(),
];
});
return [
'work_order' => [
'id' => $workOrder->id,
'order_no' => $workOrder->order_no,
'status' => $workOrder->status,
'planned_date' => $workOrder->planned_date,
'due_date' => $workOrder->due_date,
],
'order' => $workOrder->order ? [
'id' => $workOrder->order->id,
'order_no' => $workOrder->order->order_no,
'client_name' => $workOrder->order->client_name ?? null,
'site_name' => $workOrder->order->site_name ?? null,
'order_date' => $workOrder->order->order_date ?? null,
] : null,
'items' => $items,
'summary' => [
'total_items' => $items->count(),
'inspected_items' => $items->filter(fn ($i) => $i['inspection_data'] !== null)->count(),
'passed_items' => $items->filter(fn ($i) => ($i['inspection_data']['judgment'] ?? null) === 'pass')->count(),
'failed_items' => $items->filter(fn ($i) => ($i['inspection_data']['judgment'] ?? null) === 'fail')->count(),
],
];
}
} }

View File

@@ -438,6 +438,7 @@
'item_status_updated' => '품목 상태가 변경되었습니다.', 'item_status_updated' => '품목 상태가 변경되었습니다.',
'materials_fetched' => '자재 목록을 조회했습니다.', 'materials_fetched' => '자재 목록을 조회했습니다.',
'material_input_registered' => '자재 투입이 등록되었습니다.', 'material_input_registered' => '자재 투입이 등록되었습니다.',
'inspection_saved' => '검사 데이터가 저장되었습니다.',
], ],
// 검사 관리 // 검사 관리

View File

@@ -71,6 +71,11 @@
// 공정 단계 진행 관리 // 공정 단계 진행 관리
Route::get('/{id}/step-progress', [WorkOrderController::class, 'stepProgress'])->whereNumber('id')->name('v1.work-orders.step-progress'); // 단계 진행 조회 Route::get('/{id}/step-progress', [WorkOrderController::class, 'stepProgress'])->whereNumber('id')->name('v1.work-orders.step-progress'); // 단계 진행 조회
Route::patch('/{id}/step-progress/{progressId}/toggle', [WorkOrderController::class, 'toggleStepProgress'])->whereNumber('id')->whereNumber('progressId')->name('v1.work-orders.step-progress.toggle'); // 단계 토글 Route::patch('/{id}/step-progress/{progressId}/toggle', [WorkOrderController::class, 'toggleStepProgress'])->whereNumber('id')->whereNumber('progressId')->name('v1.work-orders.step-progress.toggle'); // 단계 토글
// 중간검사 관리
Route::post('/{id}/items/{itemId}/inspection', [WorkOrderController::class, 'storeItemInspection'])->whereNumber('id')->whereNumber('itemId')->name('v1.work-orders.items.inspection'); // 품목 검사 저장
Route::get('/{id}/inspection-data', [WorkOrderController::class, 'inspectionData'])->whereNumber('id')->name('v1.work-orders.inspection-data'); // 검사 데이터 조회
Route::get('/{id}/inspection-report', [WorkOrderController::class, 'inspectionReport'])->whereNumber('id')->name('v1.work-orders.inspection-report'); // 검사 성적서 조회
}); });
// Work Result API (작업실적 관리) // Work Result API (작업실적 관리)