feat(API): 작업일지 생성/조회 API 추가

- WorkOrderService: getWorkLogTemplate, getWorkLog, createWorkLog 메서드 추가
- WorkOrderController: 작업일지 3개 엔드포인트 추가
- 라우트: GET work-log-template, GET/POST work-log
- WorkOrder 모델: documents() MorphMany 관계 추가
- i18n: work_log_saved, no_work_log_template 메시지 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-12 09:39:48 +09:00
parent 911c8a36ad
commit 45dd18dbab
6 changed files with 367 additions and 0 deletions

View File

@@ -2155,4 +2155,324 @@ public function getInspectionReport(int $workOrderId): array
],
];
}
// ──────────────────────────────────────────────────────────────
// 작업일지 (Work Log)
// ──────────────────────────────────────────────────────────────
/**
* 작업일지 양식 템플릿 조회
*
* 공정(Process)의 work_log_template_id 기반으로 작업일지 양식을 조회하고
* 기본필드에 작업지시 정보를 자동 매핑하여 반환
*/
public function getWorkLogTemplate(int $workOrderId): array
{
$tenantId = $this->tenantId();
$workOrder = WorkOrder::where('tenant_id', $tenantId)
->with([
'process.workLogTemplateRelation' => fn ($q) => $q->with([
'approvalLines',
'basicFields',
'columns',
]),
'salesOrder:id,order_no,client_name,site_name,delivery_date',
'items:id,work_order_id,item_name,specification,quantity,unit,sort_order,status',
])
->find($workOrderId);
if (! $workOrder) {
throw new NotFoundHttpException(__('error.not_found'));
}
$process = $workOrder->process;
$docTemplate = $process?->workLogTemplateRelation;
if (! $docTemplate) {
return [
'work_order_id' => $workOrderId,
'has_template' => false,
'template' => null,
'work_order_info' => $this->buildWorkOrderInfo($workOrder),
];
}
$documentService = app(DocumentService::class);
$formattedTemplate = $documentService->formatTemplateForReact($docTemplate);
// 기본필드 자동 매핑 (발주처, 현장명, LOT NO 등)
$autoValues = $this->buildWorkLogAutoValues($workOrder);
return [
'work_order_id' => $workOrderId,
'has_template' => true,
'template' => $formattedTemplate,
'auto_values' => $autoValues,
'work_order_info' => $this->buildWorkOrderInfo($workOrder),
'work_stats' => $this->calculateWorkStats($workOrder),
];
}
/**
* 작업일지 조회 (기존 문서가 있으면 데이터 포함)
*/
public function getWorkLog(int $workOrderId): array
{
$tenantId = $this->tenantId();
$workOrder = WorkOrder::where('tenant_id', $tenantId)
->with([
'process.workLogTemplateRelation' => fn ($q) => $q->with([
'approvalLines',
'basicFields',
'columns',
]),
'salesOrder:id,order_no,client_name,site_name,delivery_date',
'items:id,work_order_id,item_name,specification,quantity,unit,sort_order,status',
])
->find($workOrderId);
if (! $workOrder) {
throw new NotFoundHttpException(__('error.not_found'));
}
$process = $workOrder->process;
$templateId = $process?->work_log_template_id;
// 기존 작업일지 문서 조회
$document = null;
if ($templateId) {
$document = Document::query()
->where('tenant_id', $tenantId)
->where('template_id', $templateId)
->where('linkable_type', 'work_order')
->where('linkable_id', $workOrderId)
->with(['approvals.user:id,name', 'data'])
->latest()
->first();
}
$docTemplate = $process?->workLogTemplateRelation;
$formattedTemplate = null;
if ($docTemplate) {
$documentService = app(DocumentService::class);
$formattedTemplate = $documentService->formatTemplateForReact($docTemplate);
}
return [
'work_order_id' => $workOrderId,
'has_template' => $docTemplate !== null,
'template' => $formattedTemplate,
'document' => $document ? [
'id' => $document->id,
'document_no' => $document->document_no,
'status' => $document->status,
'submitted_at' => $document->submitted_at,
'completed_at' => $document->completed_at,
'approvals' => $document->approvals->map(fn ($a) => [
'id' => $a->id,
'step' => $a->step,
'role' => $a->role,
'status' => $a->status,
'user' => $a->user ? ['id' => $a->user->id, 'name' => $a->user->name] : null,
'comment' => $a->comment,
'acted_at' => $a->acted_at,
])->toArray(),
'data' => $document->data->map(fn ($d) => [
'field_key' => $d->field_key,
'field_value' => $d->field_value,
'section_id' => $d->section_id,
'column_id' => $d->column_id,
'row_index' => $d->row_index,
])->toArray(),
] : null,
'auto_values' => $this->buildWorkLogAutoValues($workOrder),
'work_order_info' => $this->buildWorkOrderInfo($workOrder),
'work_stats' => $this->calculateWorkStats($workOrder),
];
}
/**
* 작업일지 생성/수정 (Document 기반)
*
* 기존 DRAFT/REJECTED 문서가 있으면 update, 없으면 create
*/
public function createWorkLog(int $workOrderId, array $workLogData): array
{
$tenantId = $this->tenantId();
$workOrder = WorkOrder::where('tenant_id', $tenantId)
->with(['process', 'salesOrder:id,order_no,client_name,site_name'])
->find($workOrderId);
if (! $workOrder) {
throw new NotFoundHttpException(__('error.not_found'));
}
$process = $workOrder->process;
$templateId = $process?->work_log_template_id;
if (! $templateId) {
throw new BadRequestHttpException(__('error.work_order.no_work_log_template'));
}
$documentService = app(DocumentService::class);
// 기존 DRAFT/REJECTED 문서 확인
$existingDocument = Document::query()
->where('tenant_id', $tenantId)
->where('template_id', $templateId)
->where('linkable_type', 'work_order')
->where('linkable_id', $workOrderId)
->whereIn('status', [Document::STATUS_DRAFT, Document::STATUS_REJECTED])
->latest()
->first();
// 작업일지 데이터를 document_data 레코드로 변환
$documentDataRecords = $this->transformWorkLogDataToRecords($workLogData, $workOrder);
if ($existingDocument) {
$document = $documentService->update($existingDocument->id, [
'title' => $workLogData['title'] ?? $existingDocument->title,
'data' => $documentDataRecords,
]);
$action = 'work_log_updated';
} else {
$document = $documentService->create([
'template_id' => $templateId,
'title' => $workLogData['title'] ?? "작업일지 - {$workOrder->work_order_no}",
'linkable_type' => 'work_order',
'linkable_id' => $workOrderId,
'data' => $documentDataRecords,
'approvers' => $workLogData['approvers'] ?? [],
]);
$action = 'work_log_created';
}
// 감사 로그
$this->auditLogger->log(
$tenantId,
self::AUDIT_TARGET,
$workOrderId,
$action,
null,
['document_id' => $document->id, 'document_no' => $document->document_no]
);
return [
'document_id' => $document->id,
'document_no' => $document->document_no,
'status' => $document->status,
'is_new' => $action === 'work_log_created',
];
}
/**
* 작업일지 기본필드 자동 매핑값 생성
*/
private function buildWorkLogAutoValues(WorkOrder $workOrder): array
{
$salesOrder = $workOrder->salesOrder;
return [
'발주처' => $salesOrder?->client_name ?? '',
'현장명' => $salesOrder?->site_name ?? '',
'작업일자' => now()->format('Y-m-d'),
'LOT NO' => '',
'납기일' => $salesOrder?->delivery_date?->format('Y-m-d') ?? '',
'작업지시번호' => $workOrder->work_order_no ?? '',
'수주일' => $salesOrder?->order_date ?? '',
'수주처' => $salesOrder?->client_name ?? '',
'담당자' => '',
'연락처' => '',
'제품 LOT NO' => '',
'생산담당자' => '',
'출고예정일' => $salesOrder?->delivery_date?->format('Y-m-d') ?? '',
];
}
/**
* 작업 통계 계산
*/
private function calculateWorkStats(WorkOrder $workOrder): array
{
$items = $workOrder->items;
if (! $items || $items->isEmpty()) {
return [
'order_qty' => 0,
'completed_qty' => 0,
'in_progress_qty' => 0,
'waiting_qty' => 0,
'progress' => 0,
];
}
$total = $items->count();
$completed = $items->where('status', 'completed')->count();
$inProgress = $items->where('status', 'in_progress')->count();
$waiting = $total - $completed - $inProgress;
return [
'order_qty' => $total,
'completed_qty' => $completed,
'in_progress_qty' => $inProgress,
'waiting_qty' => $waiting,
'progress' => $total > 0 ? round(($completed / $total) * 100, 1) : 0,
];
}
/**
* 작업일지 데이터를 document_data 레코드로 변환
*
* 입력 형식:
* basic_data: { '발주처': '...', '현장명': '...' }
* table_data: [{ item_name, floor_code, specification, quantity, status }]
* remarks: "특이사항"
*/
private function transformWorkLogDataToRecords(array $workLogData, WorkOrder $workOrder): array
{
$records = [];
// 1. 기본필드 저장
$basicData = $workLogData['basic_data'] ?? [];
foreach ($basicData as $key => $value) {
$records[] = [
'field_key' => "basic_{$key}",
'field_value' => $value,
];
}
// 2. 품목 테이블 데이터 (WorkOrderItem 기반)
$tableData = $workLogData['table_data'] ?? [];
foreach ($tableData as $index => $row) {
foreach ($row as $fieldKey => $fieldValue) {
$records[] = [
'row_index' => $index,
'field_key' => "item_{$fieldKey}",
'field_value' => is_array($fieldValue) ? json_encode($fieldValue) : (string) $fieldValue,
];
}
}
// 3. 작업 통계 (자동 계산)
$stats = $this->calculateWorkStats($workOrder);
foreach ($stats as $key => $value) {
$records[] = [
'field_key' => "stats_{$key}",
'field_value' => (string) $value,
];
}
// 4. 특이사항
if (isset($workLogData['remarks'])) {
$records[] = [
'field_key' => 'remarks',
'field_value' => $workLogData['remarks'],
];
}
return $records;
}
}