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:
@@ -258,4 +258,34 @@ public function createInspectionDocument(Request $request, int $id)
|
||||
return $this->service->createInspectionDocument($id, $request->all());
|
||||
}, __('message.work_order.inspection_document_created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업일지 양식 템플릿 조회
|
||||
*/
|
||||
public function workLogTemplate(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->getWorkLogTemplate($id);
|
||||
}, __('message.work_order.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업일지 조회 (기존 문서 + 템플릿 + 통계)
|
||||
*/
|
||||
public function workLog(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->getWorkLog($id);
|
||||
}, __('message.work_order.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업일지 생성/수정
|
||||
*/
|
||||
public function createWorkLog(Request $request, int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
return $this->service->createWorkLog($id, $request->all());
|
||||
}, __('message.work_order.work_log_saved'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models\Production;
|
||||
|
||||
use App\Models\Documents\Document;
|
||||
use App\Models\Members\User;
|
||||
use App\Models\Orders\Order;
|
||||
use App\Models\Process;
|
||||
@@ -14,6 +15,7 @@
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
@@ -203,6 +205,14 @@ public function stepProgress(): HasMany
|
||||
return $this->hasMany(WorkOrderStepProgress::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 문서 (중간검사, 작업일지 등)
|
||||
*/
|
||||
public function documents(): MorphMany
|
||||
{
|
||||
return $this->morphMany(Document::class, 'linkable');
|
||||
}
|
||||
|
||||
/**
|
||||
* 출하 목록
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,6 +424,7 @@
|
||||
'invalid_transition' => "상태를 ':from'에서 ':to'(으)로 변경할 수 없습니다. 허용된 상태: :allowed",
|
||||
'assignee_required_for_linked' => '수주 연동 등록 시 담당자를 선택해주세요.',
|
||||
'no_inspection_template' => '검사용 문서 양식이 설정되지 않았습니다.',
|
||||
'no_work_log_template' => '작업일지 양식이 설정되지 않았습니다.',
|
||||
],
|
||||
|
||||
// 검사 관련
|
||||
|
||||
@@ -440,6 +440,7 @@
|
||||
'material_input_registered' => '자재 투입이 등록되었습니다.',
|
||||
'inspection_saved' => '검사 데이터가 저장되었습니다.',
|
||||
'inspection_document_created' => '검사 문서가 생성되었습니다.',
|
||||
'work_log_saved' => '작업일지가 저장되었습니다.',
|
||||
],
|
||||
|
||||
// 검사 관리
|
||||
|
||||
@@ -80,6 +80,11 @@
|
||||
Route::get('/{id}/inspection-template', [WorkOrderController::class, 'inspectionTemplate'])->whereNumber('id')->name('v1.work-orders.inspection-template'); // 검사 문서 템플릿 조회
|
||||
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'); // 검사 문서 생성/수정
|
||||
|
||||
// 작업일지 관리
|
||||
Route::get('/{id}/work-log-template', [WorkOrderController::class, 'workLogTemplate'])->whereNumber('id')->name('v1.work-orders.work-log-template'); // 작업일지 양식 조회
|
||||
Route::get('/{id}/work-log', [WorkOrderController::class, 'workLog'])->whereNumber('id')->name('v1.work-orders.work-log'); // 작업일지 조회
|
||||
Route::post('/{id}/work-log', [WorkOrderController::class, 'createWorkLog'])->whereNumber('id')->name('v1.work-orders.work-log.store'); // 작업일지 생성/수정
|
||||
});
|
||||
|
||||
// Work Result API (작업실적 관리)
|
||||
|
||||
Reference in New Issue
Block a user