feat: 통합 감사 로그 도입 및 조회 API/스케줄러 추가

- DB: 감사 로그 테이블(audit_logs) 마이그레이션 및 인덱스 추가
- Config: audit.php 추가(AUDIT_RETENTION_DAYS, AUDIT_LOG_READS 토글)
- Model/Service: AuditLog 모델, AuditLogger 서비스 생성
- 도메인 훅: ModelVersion.release(released), BomTemplate upsert/update/delete/replaceItems/clone 기록, diff 조회는 설정 기반 기록
- API: GET /api/v1/design/audit-logs 추가(FormRequest/Service/Controller, 필터 page/size/target_type/target_id/action/actor_id/from/to/sort/order)
- Swagger: 감사 로그 조회 문서 추가(Design Audit 태그)
- Console: audit:prune 커맨드 추가 및 스케줄러 매일 03:10 실행 등록(시스템 크론 schedule:run 필요)
- Fix: PruneAuditLogs import 충돌 제거(Google ServiceControl AuditLog 제거)
This commit is contained in:
2025-09-11 14:39:55 +09:00
parent 17fa82c35b
commit 785e367472
13 changed files with 404 additions and 1 deletions

View File

@@ -51,6 +51,9 @@ public function upsertTemplate(int $modelVersionId, string $name = 'Main', bool
->where('name', $name)
->first();
$action = 'created';
$before = null;
if (!$tpl) {
$tpl = BomTemplate::create([
'tenant_id' => $tenantId,
@@ -60,6 +63,8 @@ public function upsertTemplate(int $modelVersionId, string $name = 'Main', bool
'notes' => $notes,
]);
} else {
$action = 'updated';
$before = $tpl->toArray();
$tpl->fill(['is_primary' => $isPrimary, 'notes' => $notes])->save();
}
@@ -70,6 +75,16 @@ public function upsertTemplate(int $modelVersionId, string $name = 'Main', bool
->update(['is_primary' => false]);
}
// 감사 로그
app(\App\Services\Audit\AuditLogger::class)->log(
tenantId: $tenantId,
targetType: 'bom_template',
targetId: $tpl->id,
action: $action,
before: $before,
after: $tpl->toArray()
);
return $tpl;
});
}
@@ -99,6 +114,7 @@ public function updateTemplate(int $templateId, array $data): BomTemplate
}
}
$before = $tpl->toArray();
$tpl->fill($data)->save();
if (array_key_exists('is_primary', $data) && $data['is_primary']) {
@@ -109,6 +125,16 @@ public function updateTemplate(int $templateId, array $data): BomTemplate
->update(['is_primary' => false]);
}
// 감사 로그
app(\App\Services\Audit\AuditLogger::class)->log(
tenantId: $tenantId,
targetType: 'bom_template',
targetId: $tpl->id,
action: 'updated',
before: $before,
after: $tpl->toArray()
);
return $tpl;
}
@@ -125,7 +151,18 @@ public function deleteTemplate(int $templateId): void
throw new NotFoundHttpException(__('error.not_found'));
}
$before = $tpl->toArray();
$tpl->delete();
// 감사 로그
app(\App\Services\Audit\AuditLogger::class)->log(
tenantId: $tenantId,
targetType: 'bom_template',
targetId: $tpl->id,
action: 'deleted',
before: $before,
after: null
);
}
/** 템플릿 상세 (옵션: 항목 포함) */
@@ -214,6 +251,26 @@ public function replaceItems(int $templateId, array $items): void
if (!empty($payloads)) {
BomTemplateItem::insert($payloads);
}
// 감사 로그
app(\App\Services\Audit\AuditLogger::class)->log(
tenantId: $tenantId,
targetType: 'bom_template',
targetId: $tpl->id,
action: 'items_replaced',
before: ['items' => $beforeItems],
after: ['items' => array_map(function ($p) {
return [
'ref_type' => $p['ref_type'],
'ref_id' => $p['ref_id'],
'qty' => (float) $p['qty'],
'waste_rate' => (float) $p['waste_rate'],
'uom_id' => $p['uom_id'],
'notes' => $p['notes'],
'sort_order' => $p['sort_order'],
];
}, $payloads)]
);
});
}
@@ -309,7 +366,7 @@ public function diffTemplates(int $leftTemplateId, int $rightTemplateId): array
}
}
return [
$result = [
'left_template_id' => $left->id,
'right_template_id' => $right->id,
'summary' => [
@@ -321,6 +378,19 @@ public function diffTemplates(int $leftTemplateId, int $rightTemplateId): array
'removed' => $removed,
'changed' => $changed,
];
if (config('audit.log_reads', false)) {
app(\App\Services\Audit\AuditLogger::class)->log(
tenantId: $tenantId,
targetType: 'bom_template',
targetId: $left->id,
action: 'diff_viewed',
before: ['left' => $left->id, 'right' => $right->id],
after: ['summary' => $result['summary']]
);
}
return $result;
}
/** 템플릿 복제(깊은 복사) */
@@ -394,6 +464,16 @@ public function cloneTemplate(int $templateId, ?int $targetVersionId = null, ?st
->update(['is_primary' => false]);
}
// 감사 로그
app(\App\Services\Audit\AuditLogger::class)->log(
tenantId: $tenantId,
targetType: 'bom_template',
targetId: $dest->id,
action: 'cloned',
before: ['source_template_id' => $src->id],
after: $dest->toArray()
);
return $dest;
});
}