feat:중간검사 API 다중단계/resolve/upsert 지원 (Phase 5.1.3)

- getInspectionTemplate: 전체 검사 단계 templates[] 반환 (기존 첫번째만→다중)
- resolveInspectionDocument 신규: step_id 기반 기존 문서 조회 또는 템플릿 반환
- createInspectionDocument 개선: step_id 파라미터, 기존 DRAFT/REJECTED 문서 update 지원
- GET inspection-resolve 라우트 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-10 16:55:29 +09:00
parent d8fd221278
commit 6733a431bb
3 changed files with 144 additions and 30 deletions

View File

@@ -230,7 +230,17 @@ public function inspectionTemplate(int $id)
}
/**
* 검사 완료 시 검사 문서(Document) 생성
* 검사 문서 resolve (기존 문서 조회 또는 생성 정보 반환)
*/
public function resolveInspectionDocument(Request $request, int $id)
{
return ApiResponse::handle(function () use ($request, $id) {
return $this->service->resolveInspectionDocument($id, $request->all());
}, __('message.fetched'));
}
/**
* 검사 완료 시 검사 문서(Document) 생성/수정
*/
public function createInspectionDocument(Request $request, int $id)
{

View File

@@ -2,6 +2,7 @@
namespace App\Services;
use App\Models\Documents\Document;
use App\Models\Documents\DocumentTemplate;
use App\Models\Orders\Order;
use App\Models\Production\WorkOrder;
@@ -1630,6 +1631,7 @@ public function getInspectionData(int $workOrderId, array $params = []): array
* 작업지시의 검사용 문서 템플릿 조회
*
* work_order → process → steps(needs_inspection=true) → documentTemplate 로드
* 모든 검사 단계의 템플릿을 반환 (다중 검사 단계 지원)
*/
public function getInspectionTemplate(int $workOrderId): array
{
@@ -1662,38 +1664,112 @@ public function getInspectionTemplate(int $workOrderId): array
return [
'work_order_id' => $workOrderId,
'has_template' => false,
'templates' => [],
'template' => null,
'work_order_info' => $this->buildWorkOrderInfo($workOrder),
];
}
// 첫 번째 검사 단계의 템플릿 사용 (향후 다중 검사 단계 지원 가능)
$inspectionStep = $inspectionSteps->first();
$template = $inspectionStep->documentTemplate;
if (! $template) {
return [
'work_order_id' => $workOrderId,
'has_template' => false,
'template' => null,
'work_order_info' => $this->buildWorkOrderInfo($workOrder),
];
}
// DocumentService의 formatTemplateForReact와 동일한 포맷
$documentService = app(DocumentService::class);
$formattedTemplate = $documentService->formatTemplateForReact($template);
$templates = [];
foreach ($inspectionSteps as $step) {
if (! $step->documentTemplate) {
continue;
}
$templates[] = [
'step_id' => $step->id,
'step_name' => $step->step_name,
'step_code' => $step->step_code,
'sort_order' => $step->sort_order,
'template' => $documentService->formatTemplateForReact($step->documentTemplate),
];
}
return [
'work_order_id' => $workOrderId,
'has_template' => true,
'has_template' => ! empty($templates),
'templates' => $templates,
// 하위호환: 첫 번째 템플릿
'template' => ! empty($templates) ? $templates[0]['template'] : null,
'work_order_info' => $this->buildWorkOrderInfo($workOrder),
];
}
/**
* 작업지시 검사 문서 resolve (기존 문서 조회 또는 생성 정보 반환)
*
* step_id 기반으로 해당 검사 단계의 템플릿과 기존 문서를 조회.
* 기존 DRAFT/REJECTED 문서가 있으면 반환, 없으면 template만 반환.
*/
public function resolveInspectionDocument(int $workOrderId, array $params = []): array
{
$tenantId = $this->tenantId();
$workOrder = WorkOrder::where('tenant_id', $tenantId)
->with([
'process.steps' => fn ($q) => $q->where('is_active', true)
->where('needs_inspection', true)
->whereNotNull('document_template_id')
->orderBy('sort_order'),
'process.steps.documentTemplate' => fn ($q) => $q->with([
'approvalLines',
'basicFields',
'sections.items',
'columns',
'sectionFields',
]),
'salesOrder:id,order_no,client_name,site_name',
'items:id,work_order_id,item_name,specification,quantity,unit,sort_order',
])
->find($workOrderId);
if (! $workOrder) {
throw new NotFoundHttpException(__('error.not_found'));
}
// step_id가 지정되면 해당 단계 사용, 아니면 첫 번째 검사 단계
$inspectionSteps = $workOrder->process?->steps ?? collect();
$stepId = $params['step_id'] ?? null;
$inspectionStep = $stepId
? $inspectionSteps->firstWhere('id', (int) $stepId)
: $inspectionSteps->first();
if (! $inspectionStep || ! $inspectionStep->document_template_id) {
throw new BadRequestHttpException(__('error.work_order.no_inspection_template'));
}
$documentService = app(DocumentService::class);
$formattedTemplate = $documentService->formatTemplateForReact($inspectionStep->documentTemplate);
// 기존 문서 조회 (work_order + template, 수정 가능한 상태)
$existingDocument = Document::query()
->where('tenant_id', $tenantId)
->where('template_id', $inspectionStep->document_template_id)
->where('linkable_type', 'work_order')
->where('linkable_id', $workOrderId)
->whereIn('status', [Document::STATUS_DRAFT, Document::STATUS_REJECTED])
->with(['data', 'attachments.file', 'approvals.user:id,name'])
->latest()
->first();
return [
'work_order_id' => $workOrderId,
'step_id' => $inspectionStep->id,
'step_name' => $inspectionStep->step_name,
'template' => $formattedTemplate,
'existing_document' => $existingDocument,
'work_order_info' => $this->buildWorkOrderInfo($workOrder),
];
}
/**
* 검사 완료 시 Document + DocumentData 생성
*
* step_id 지정 시 해당 단계의 템플릿 사용, 미지정 시 첫 번째 검사 단계 사용.
* 기존 DRAFT/REJECTED 문서가 있으면 update, 없으면 create.
*/
public function createInspectionDocument(int $workOrderId, array $inspectionData): array
{
@@ -1713,31 +1789,57 @@ public function createInspectionDocument(int $workOrderId, array $inspectionData
throw new NotFoundHttpException(__('error.not_found'));
}
$inspectionStep = $workOrder->process?->steps?->first();
// step_id가 지정되면 해당 단계 사용, 아니면 첫 번째
$inspectionSteps = $workOrder->process?->steps ?? collect();
$stepId = $inspectionData['step_id'] ?? null;
$inspectionStep = $stepId
? $inspectionSteps->firstWhere('id', (int) $stepId)
: $inspectionSteps->first();
if (! $inspectionStep || ! $inspectionStep->document_template_id) {
throw new BadRequestHttpException(__('error.work_order.no_inspection_template'));
}
$documentService = app(DocumentService::class);
// DocumentService::create() 재사용
$documentData = [
'template_id' => $inspectionStep->document_template_id,
'title' => $inspectionData['title'] ?? "중간검사성적서 - {$workOrder->work_order_no}",
'linkable_type' => 'work_order',
'linkable_id' => $workOrderId,
'data' => $inspectionData['data'] ?? [],
'approvers' => $inspectionData['approvers'] ?? [],
];
// 기존 DRAFT/REJECTED 문서가 있으면 update
$existingDocument = Document::query()
->where('tenant_id', $tenantId)
->where('template_id', $inspectionStep->document_template_id)
->where('linkable_type', 'work_order')
->where('linkable_id', $workOrderId)
->whereIn('status', [Document::STATUS_DRAFT, Document::STATUS_REJECTED])
->latest()
->first();
$document = $documentService->create($documentData);
if ($existingDocument) {
$document = $documentService->update($existingDocument->id, [
'title' => $inspectionData['title'] ?? $existingDocument->title,
'data' => $inspectionData['data'] ?? [],
]);
$action = 'inspection_document_updated';
} else {
$documentData = [
'template_id' => $inspectionStep->document_template_id,
'title' => $inspectionData['title'] ?? "중간검사성적서 - {$workOrder->work_order_no}",
'linkable_type' => 'work_order',
'linkable_id' => $workOrderId,
'data' => $inspectionData['data'] ?? [],
'approvers' => $inspectionData['approvers'] ?? [],
];
$document = $documentService->create($documentData);
$action = 'inspection_document_created';
}
// 감사 로그
$this->auditLogger->log(
$tenantId,
self::AUDIT_TARGET,
$workOrderId,
'inspection_document_created',
$action,
null,
['document_id' => $document->id, 'document_no' => $document->document_no]
);
@@ -1746,6 +1848,7 @@ public function createInspectionDocument(int $workOrderId, array $inspectionData
'document_id' => $document->id,
'document_no' => $document->document_no,
'status' => $document->status,
'is_new' => $action === 'inspection_document_created',
];
}

View File

@@ -77,7 +77,8 @@
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'); // 검사 성적서 조회
Route::get('/{id}/inspection-template', [WorkOrderController::class, 'inspectionTemplate'])->whereNumber('id')->name('v1.work-orders.inspection-template'); // 검사 문서 템플릿 조회
Route::post('/{id}/inspection-document', [WorkOrderController::class, 'createInspectionDocument'])->whereNumber('id')->name('v1.work-orders.inspection-document'); // 검사 문서 생성
Route::get('/{id}/inspection-resolve', [WorkOrderController::class, 'resolveInspectionDocument'])->whereNumber('id')->name('v1.work-orders.inspection-resolve'); // 검사 문서 resolve (기존 문서/템플릿)
Route::post('/{id}/inspection-document', [WorkOrderController::class, 'createInspectionDocument'])->whereNumber('id')->name('v1.work-orders.inspection-document'); // 검사 문서 생성/수정
});
// Work Result API (작업실적 관리)