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:
@@ -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'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
51
app/Http/Requests/WorkOrder/StoreItemInspectionRequest.php
Normal file
51
app/Http/Requests/WorkOrder/StoreItemInspectionRequest.php
Normal 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' => '검사 데이터']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 작업 결과 데이터 가져오기
|
* 작업 결과 데이터 가져오기
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -438,6 +438,7 @@
|
|||||||
'item_status_updated' => '품목 상태가 변경되었습니다.',
|
'item_status_updated' => '품목 상태가 변경되었습니다.',
|
||||||
'materials_fetched' => '자재 목록을 조회했습니다.',
|
'materials_fetched' => '자재 목록을 조회했습니다.',
|
||||||
'material_input_registered' => '자재 투입이 등록되었습니다.',
|
'material_input_registered' => '자재 투입이 등록되었습니다.',
|
||||||
|
'inspection_saved' => '검사 데이터가 저장되었습니다.',
|
||||||
],
|
],
|
||||||
|
|
||||||
// 검사 관리
|
// 검사 관리
|
||||||
|
|||||||
@@ -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 (작업실적 관리)
|
||||||
|
|||||||
Reference in New Issue
Block a user