feat: [material] 부적합관리 결재 연동 구현

- Migration: approval_id FK 추가
- Model: approval() BelongsTo 관계
- Service: submitForApproval() 결재상신 (결재문서+결재선 생성)
- ApprovalService: 승인→CLOSED, 반려/회수→approval_id 해제
- Controller: POST /{id}/submit-approval 엔드포인트
- Route: submit-approval 라우트 등록
This commit is contained in:
김보곤
2026-03-19 09:03:12 +09:00
parent 847c60b03d
commit 6e50fbd1fa
7 changed files with 210 additions and 0 deletions

View File

@@ -4,6 +4,9 @@
use App\Models\Materials\NonconformingReport;
use App\Models\Materials\NonconformingReportItem;
use App\Models\Tenants\Approval;
use App\Models\Tenants\ApprovalLine;
use App\Models\Tenants\ApprovalStep;
use Illuminate\Support\Facades\DB;
class NonconformingReportService extends Service
@@ -68,6 +71,9 @@ public function show(int $id): NonconformingReport
'actionManager:id,name',
'relatedEmployee:id,name',
'files',
'approval:id,document_number,status,completed_at',
'approval.steps:id,approval_id,step_order,step_type,approver_id,status,comment,acted_at',
'approval.steps.approver:id,name',
])
->findOrFail($id);
}
@@ -235,6 +241,80 @@ public function stats(array $params): array
];
}
/**
* 결재상신 (RESOLVED 상태에서만 가능)
*/
public function submitForApproval(int $id, array $data): NonconformingReport
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($id, $data, $tenantId, $userId) {
$report = NonconformingReport::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
if ($report->status !== NonconformingReport::STATUS_RESOLVED) {
abort(422, __('error.nonconforming.must_be_resolved_for_approval'));
}
if ($report->approval_id) {
abort(422, __('error.nonconforming.approval_already_exists'));
}
$steps = $data['steps'] ?? [];
if (empty($steps)) {
abort(422, __('error.approval.steps_required'));
}
// 결재 문서 생성
$approval = Approval::create([
'tenant_id' => $tenantId,
'document_number' => $this->generateApprovalNumber($tenantId),
'form_id' => $data['form_id'] ?? null,
'title' => $data['title'] ?? "부적합 처리 결재 - {$report->nc_number}",
'content' => [
'nc_number' => $report->nc_number,
'nc_type' => $report->nc_type,
'site_name' => $report->site_name,
'defect_description' => $report->defect_description,
'cause_analysis' => $report->cause_analysis,
'corrective_action' => $report->corrective_action,
'total_cost' => $report->total_cost,
],
'status' => Approval::STATUS_PENDING,
'drafter_id' => $userId,
'department_id' => $report->department_id,
'drafted_at' => now(),
'current_step' => 1,
'linkable_type' => NonconformingReport::class,
'linkable_id' => $report->id,
'created_by' => $userId,
'updated_by' => $userId,
]);
// 결재선 단계 생성
foreach ($steps as $index => $step) {
ApprovalStep::create([
'tenant_id' => $tenantId,
'approval_id' => $approval->id,
'step_order' => $index + 1,
'step_type' => $step['step_type'] ?? ApprovalLine::STEP_TYPE_APPROVAL,
'approver_id' => $step['approver_id'],
'status' => ApprovalStep::STATUS_PENDING,
]);
}
// 보고서에 결재 연결
$report->update([
'approval_id' => $approval->id,
'updated_by' => $userId,
]);
return $this->show($report->id);
});
}
// ── private ──
/**
@@ -298,6 +378,26 @@ private function sumItemAmounts(array $items): int
return $total;
}
/**
* 결재 문서번호 생성
*/
private function generateApprovalNumber(int $tenantId): string
{
$prefix = 'AP';
$date = now()->format('Ymd');
$pattern = "{$prefix}-{$date}-";
$lastNumber = Approval::withTrashed()
->where('tenant_id', $tenantId)
->where('document_number', 'like', "{$pattern}%")
->orderByDesc('document_number')
->value('document_number');
$seq = $lastNumber ? ((int) substr($lastNumber, -4) + 1) : 1;
return sprintf('%s-%s-%04d', $prefix, $date, $seq);
}
/**
* 상태 전이 검증
*/