feat(API): 중간검사 문서 템플릿 동적 연동 - process_steps ↔ document_templates 연결

- process_steps 테이블에 document_template_id FK 추가 (migration)
- ProcessStep 모델에 documentTemplate BelongsTo 관계 추가
- ProcessStepService에서 documentTemplate eager loading
- StoreProcessStepRequest/UpdateProcessStepRequest에 document_template_id 유효성 검증
- WorkOrderService에 getInspectionTemplate(), createInspectionDocument() 메서드 추가
- WorkOrderController에 inspection-template/inspection-document 엔드포인트 추가
- DocumentService.formatTemplateForReact() 접근자 public으로 변경
- i18n 메시지 키 추가 (inspection_document_created, no_inspection_template)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-10 08:35:57 +09:00
parent 7adfc6d536
commit e885b1ca45
12 changed files with 232 additions and 3 deletions

View File

@@ -2,6 +2,7 @@
namespace App\Services;
use App\Models\Documents\DocumentTemplate;
use App\Models\Orders\Order;
use App\Models\Production\WorkOrder;
use App\Models\Production\WorkOrderAssignee;
@@ -1621,6 +1622,159 @@ public function getInspectionData(int $workOrderId, array $params = []): array
];
}
// ──────────────────────────────────────────────────────────────
// 검사 문서 템플릿 연동
// ──────────────────────────────────────────────────────────────
/**
* 작업지시의 검사용 문서 템플릿 조회
*
* work_order → process → steps(needs_inspection=true) → documentTemplate 로드
*/
public function getInspectionTemplate(int $workOrderId): 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'));
}
$inspectionSteps = $workOrder->process?->steps ?? collect();
if ($inspectionSteps->isEmpty()) {
return [
'work_order_id' => $workOrderId,
'has_template' => false,
'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);
return [
'work_order_id' => $workOrderId,
'has_template' => true,
'template' => $formattedTemplate,
'work_order_info' => $this->buildWorkOrderInfo($workOrder),
];
}
/**
* 검사 완료 시 Document + DocumentData 생성
*/
public function createInspectionDocument(int $workOrderId, array $inspectionData): array
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$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'),
])
->find($workOrderId);
if (! $workOrder) {
throw new NotFoundHttpException(__('error.not_found'));
}
$inspectionStep = $workOrder->process?->steps?->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'] ?? [],
];
$document = $documentService->create($documentData);
// 감사 로그
$this->auditLogger->log(
$tenantId,
self::AUDIT_TARGET,
$workOrderId,
'inspection_document_created',
null,
['document_id' => $document->id, 'document_no' => $document->document_no]
);
return [
'document_id' => $document->id,
'document_no' => $document->document_no,
'status' => $document->status,
];
}
/**
* 작업지시 기본정보 빌드 (검사 문서 렌더링용)
*/
private function buildWorkOrderInfo(WorkOrder $workOrder): array
{
return [
'id' => $workOrder->id,
'work_order_no' => $workOrder->work_order_no,
'project_name' => $workOrder->project_name,
'status' => $workOrder->status,
'scheduled_date' => $workOrder->scheduled_date,
'sales_order' => $workOrder->salesOrder ? [
'order_no' => $workOrder->salesOrder->order_no,
'client_name' => $workOrder->salesOrder->client_name,
'site_name' => $workOrder->salesOrder->site_name,
] : null,
'items' => $workOrder->items?->map(fn ($item) => [
'id' => $item->id,
'item_name' => $item->item_name,
'specification' => $item->specification,
'quantity' => $item->quantity,
'unit' => $item->unit,
])->toArray() ?? [],
];
}
/**
* 작업지시 검사 성적서용 데이터 조회 (전체 품목 + 검사 데이터 + 주문 정보)
*/