diff --git a/LOGICAL_RELATIONSHIPS.md b/LOGICAL_RELATIONSHIPS.md index bf16312..924666b 100644 --- a/LOGICAL_RELATIONSHIPS.md +++ b/LOGICAL_RELATIONSHIPS.md @@ -1,6 +1,6 @@ # 논리적 데이터베이스 관계 문서 -> **자동 생성**: 2026-02-07 01:10:55 +> **자동 생성**: 2026-02-09 22:02:49 > **소스**: Eloquent 모델 관계 분석 ## 📊 모델별 관계 현황 @@ -594,6 +594,7 @@ ### process_steps **모델**: `App\Models\ProcessStep` - **process()**: belongsTo → `processes` +- **documentTemplate()**: belongsTo → `document_templates` ### work_orders **모델**: `App\Models\Production\WorkOrder` @@ -635,6 +636,7 @@ ### work_order_items - **workOrder()**: belongsTo → `work_orders` - **item()**: belongsTo → `items` +- **sourceOrderItem()**: belongsTo → `order_items` ### work_order_step_progress **모델**: `App\Models\Production\WorkOrderStepProgress` @@ -782,6 +784,11 @@ ### ai_token_usages - **creator()**: belongsTo → `users` +### ai_voice_recordings +**모델**: `App\Models\Tenants\AiVoiceRecording` + +- **user()**: belongsTo → `users` + ### approvals **모델**: `App\Models\Tenants\Approval` diff --git a/app/Http/Controllers/Api/V1/WorkOrderController.php b/app/Http/Controllers/Api/V1/WorkOrderController.php index 11fa921..e9e386a 100644 --- a/app/Http/Controllers/Api/V1/WorkOrderController.php +++ b/app/Http/Controllers/Api/V1/WorkOrderController.php @@ -218,4 +218,24 @@ public function inspectionReport(int $id) return $this->service->getInspectionReport($id); }, __('message.work_order.fetched')); } + + /** + * 작업지시의 검사용 문서 템플릿 조회 + */ + public function inspectionTemplate(int $id) + { + return ApiResponse::handle(function () use ($id) { + return $this->service->getInspectionTemplate($id); + }, __('message.work_order.fetched')); + } + + /** + * 검사 완료 시 검사 문서(Document) 생성 + */ + public function createInspectionDocument(Request $request, int $id) + { + return ApiResponse::handle(function () use ($request, $id) { + return $this->service->createInspectionDocument($id, $request->all()); + }, __('message.work_order.inspection_document_created')); + } } diff --git a/app/Http/Requests/V1/ProcessStep/StoreProcessStepRequest.php b/app/Http/Requests/V1/ProcessStep/StoreProcessStepRequest.php index 543bf95..f7a261d 100644 --- a/app/Http/Requests/V1/ProcessStep/StoreProcessStepRequest.php +++ b/app/Http/Requests/V1/ProcessStep/StoreProcessStepRequest.php @@ -18,6 +18,7 @@ public function rules(): array 'is_required' => ['nullable', 'boolean'], 'needs_approval' => ['nullable', 'boolean'], 'needs_inspection' => ['nullable', 'boolean'], + 'document_template_id' => ['nullable', 'integer', 'exists:document_templates,id'], 'is_active' => ['nullable', 'boolean'], 'connection_type' => ['nullable', 'string', 'max:20'], 'connection_target' => ['nullable', 'string', 'max:255'], @@ -32,6 +33,7 @@ public function attributes(): array 'is_required' => '필수여부', 'needs_approval' => '승인필요여부', 'needs_inspection' => '검사필요여부', + 'document_template_id' => '문서양식', 'is_active' => '사용여부', 'connection_type' => '연결유형', 'connection_target' => '연결대상', diff --git a/app/Http/Requests/V1/ProcessStep/UpdateProcessStepRequest.php b/app/Http/Requests/V1/ProcessStep/UpdateProcessStepRequest.php index 0fc3ca7..4103802 100644 --- a/app/Http/Requests/V1/ProcessStep/UpdateProcessStepRequest.php +++ b/app/Http/Requests/V1/ProcessStep/UpdateProcessStepRequest.php @@ -18,6 +18,7 @@ public function rules(): array 'is_required' => ['nullable', 'boolean'], 'needs_approval' => ['nullable', 'boolean'], 'needs_inspection' => ['nullable', 'boolean'], + 'document_template_id' => ['nullable', 'integer', 'exists:document_templates,id'], 'is_active' => ['nullable', 'boolean'], 'connection_type' => ['nullable', 'string', 'max:20'], 'connection_target' => ['nullable', 'string', 'max:255'], @@ -32,6 +33,7 @@ public function attributes(): array 'is_required' => '필수여부', 'needs_approval' => '승인필요여부', 'needs_inspection' => '검사필요여부', + 'document_template_id' => '문서양식', 'is_active' => '사용여부', 'connection_type' => '연결유형', 'connection_target' => '연결대상', diff --git a/app/Models/ProcessStep.php b/app/Models/ProcessStep.php index 953fda9..bfc6137 100644 --- a/app/Models/ProcessStep.php +++ b/app/Models/ProcessStep.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\Documents\DocumentTemplate; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -17,6 +18,7 @@ class ProcessStep extends Model 'is_required', 'needs_approval', 'needs_inspection', + 'document_template_id', 'is_active', 'sort_order', 'connection_type', @@ -39,4 +41,12 @@ public function process(): BelongsTo { return $this->belongsTo(Process::class); } + + /** + * 문서 양식 (검사 시 사용할 템플릿) + */ + public function documentTemplate(): BelongsTo + { + return $this->belongsTo(DocumentTemplate::class); + } } diff --git a/app/Services/DocumentService.php b/app/Services/DocumentService.php index 36689cc..012ffd6 100644 --- a/app/Services/DocumentService.php +++ b/app/Services/DocumentService.php @@ -608,7 +608,7 @@ public function upsert(array $data): Document /** * 템플릿을 React 응답용으로 포맷 */ - private function formatTemplateForReact(DocumentTemplate $template): array + public function formatTemplateForReact(DocumentTemplate $template): array { // common_codes에서 inspection_method 코드 목록 조회 (code → name 매핑) $tenantId = $this->tenantId(); diff --git a/app/Services/ProcessStepService.php b/app/Services/ProcessStepService.php index 44fc288..8402954 100644 --- a/app/Services/ProcessStepService.php +++ b/app/Services/ProcessStepService.php @@ -17,6 +17,7 @@ public function index(int $processId) $process = $this->findProcess($processId); return $process->steps() + ->with('documentTemplate:id,name,category') ->orderBy('sort_order') ->get(); } @@ -28,7 +29,9 @@ public function show(int $processId, int $stepId) { $this->findProcess($processId); - $step = ProcessStep::where('process_id', $processId)->find($stepId); + $step = ProcessStep::where('process_id', $processId) + ->with('documentTemplate:id,name,category') + ->find($stepId); if (! $step) { throw new NotFoundHttpException(__('error.not_found')); } diff --git a/app/Services/WorkOrderService.php b/app/Services/WorkOrderService.php index d0ee318..3d5ba14 100644 --- a/app/Services/WorkOrderService.php +++ b/app/Services/WorkOrderService.php @@ -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() ?? [], + ]; + } + /** * 작업지시 검사 성적서용 데이터 조회 (전체 품목 + 검사 데이터 + 주문 정보) */ diff --git a/database/migrations/2026_02_10_000001_add_document_template_id_to_process_steps.php b/database/migrations/2026_02_10_000001_add_document_template_id_to_process_steps.php new file mode 100644 index 0000000..47befdb --- /dev/null +++ b/database/migrations/2026_02_10_000001_add_document_template_id_to_process_steps.php @@ -0,0 +1,27 @@ +foreignId('document_template_id') + ->nullable() + ->after('needs_inspection') + ->constrained('document_templates') + ->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('process_steps', function (Blueprint $table) { + $table->dropForeign(['document_template_id']); + $table->dropColumn('document_template_id'); + }); + } +}; diff --git a/lang/ko/error.php b/lang/ko/error.php index ab2dd98..5bee37e 100644 --- a/lang/ko/error.php +++ b/lang/ko/error.php @@ -423,6 +423,7 @@ 'not_bending_process' => '벤딩 공정이 아닙니다.', 'invalid_transition' => "상태를 ':from'에서 ':to'(으)로 변경할 수 없습니다. 허용된 상태: :allowed", 'assignee_required_for_linked' => '수주 연동 등록 시 담당자를 선택해주세요.', + 'no_inspection_template' => '검사용 문서 양식이 설정되지 않았습니다.', ], // 검사 관련 diff --git a/lang/ko/message.php b/lang/ko/message.php index b5e025f..95db603 100644 --- a/lang/ko/message.php +++ b/lang/ko/message.php @@ -439,6 +439,7 @@ 'materials_fetched' => '자재 목록을 조회했습니다.', 'material_input_registered' => '자재 투입이 등록되었습니다.', 'inspection_saved' => '검사 데이터가 저장되었습니다.', + 'inspection_document_created' => '검사 문서가 생성되었습니다.', ], // 검사 관리 diff --git a/routes/api/v1/production.php b/routes/api/v1/production.php index 2f9e961..b970abc 100644 --- a/routes/api/v1/production.php +++ b/routes/api/v1/production.php @@ -76,6 +76,8 @@ 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'); // 검사 성적서 조회 + 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'); // 검사 문서 생성 }); // Work Result API (작업실적 관리)