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:
24
app/Console/Commands/PruneAuditLogs.php
Normal file
24
app/Console/Commands/PruneAuditLogs.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Audit\AuditLog;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
|
class PruneAuditLogs extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'audit:prune {--days=}';
|
||||||
|
protected $description = 'Delete audit logs older than given days (default: config(audit.retention_days)).';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$days = (int) ($this->option('days') ?? config('audit.retention_days', 395));
|
||||||
|
$cutoff = Carbon::now()->subDays($days);
|
||||||
|
|
||||||
|
$count = AuditLog::query()->where('created_at', '<', $cutoff)->delete();
|
||||||
|
$this->info("Pruned {$count} audit log rows older than {$days} days.");
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
app/Console/Kernel.php
Normal file
26
app/Console/Kernel.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console;
|
||||||
|
|
||||||
|
use App\Console\Commands\PruneAuditLogs;
|
||||||
|
use Illuminate\Console\Scheduling\Schedule;
|
||||||
|
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||||
|
|
||||||
|
class Kernel extends ConsoleKernel
|
||||||
|
{
|
||||||
|
protected $commands = [
|
||||||
|
PruneAuditLogs::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function schedule(Schedule $schedule): void
|
||||||
|
{
|
||||||
|
// 매일 새벽 03:10에 감사 로그 정리(환경값 기반 보관기간)
|
||||||
|
$schedule->command('audit:prune')->dailyAt('03:10');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function commands(): void
|
||||||
|
{
|
||||||
|
// 라우트 콘솔 혹은 커맨드 자동 로딩 필요 시 사용
|
||||||
|
// $this->load(__DIR__.'/Commands');
|
||||||
|
}
|
||||||
|
}
|
||||||
23
app/Http/Controllers/Api/V1/Design/AuditLogController.php
Normal file
23
app/Http/Controllers/Api/V1/Design/AuditLogController.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1\Design;
|
||||||
|
|
||||||
|
use App\Helpers\ApiResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Audit\AuditLogIndexRequest;
|
||||||
|
use App\Services\Audit\AuditLogService;
|
||||||
|
|
||||||
|
class AuditLogController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected AuditLogService $service
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function index(AuditLogIndexRequest $request)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
$filters = $request->validated();
|
||||||
|
return $this->service->paginate($filters);
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
}
|
||||||
29
app/Http/Requests/Audit/AuditLogIndexRequest.php
Normal file
29
app/Http/Requests/Audit/AuditLogIndexRequest.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Audit;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class AuditLogIndexRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'page' => 'nullable|integer|min:1',
|
||||||
|
'size' => 'nullable|integer|min:1|max:200',
|
||||||
|
'target_type' => 'nullable|string|max:100',
|
||||||
|
'target_id' => 'nullable|integer|min:1',
|
||||||
|
'action' => 'nullable|string|max:50',
|
||||||
|
'actor_id' => 'nullable|integer|min:1',
|
||||||
|
'from' => 'nullable|date',
|
||||||
|
'to' => 'nullable|date|after_or_equal:from',
|
||||||
|
'sort' => 'nullable|string|in:created_at',
|
||||||
|
'order' => 'nullable|string|in:asc,desc',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
22
app/Models/Audit/AuditLog.php
Normal file
22
app/Models/Audit/AuditLog.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\Audit;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class AuditLog extends Model
|
||||||
|
{
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
protected $table = 'audit_logs';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'tenant_id','target_type','target_id','action','before','after','actor_id','ip','ua','created_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'before' => 'array',
|
||||||
|
'after' => 'array',
|
||||||
|
'created_at' => 'datetime',
|
||||||
|
];
|
||||||
|
}
|
||||||
43
app/Services/Audit/AuditLogService.php
Normal file
43
app/Services/Audit/AuditLogService.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Audit;
|
||||||
|
|
||||||
|
use App\Models\Audit\AuditLog;
|
||||||
|
use App\Services\Service;
|
||||||
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
|
|
||||||
|
class AuditLogService extends Service
|
||||||
|
{
|
||||||
|
public function paginate(array $filters): LengthAwarePaginator
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
|
||||||
|
$page = (int)($filters['page'] ?? 1);
|
||||||
|
$size = (int)($filters['size'] ?? 20);
|
||||||
|
$sort = $filters['sort'] ?? 'created_at';
|
||||||
|
$order = $filters['order'] ?? 'desc';
|
||||||
|
|
||||||
|
$q = AuditLog::query()->where('tenant_id', $tenantId);
|
||||||
|
|
||||||
|
if (!empty($filters['target_type'])) {
|
||||||
|
$q->where('target_type', $filters['target_type']);
|
||||||
|
}
|
||||||
|
if (!empty($filters['target_id'])) {
|
||||||
|
$q->where('target_id', (int)$filters['target_id']);
|
||||||
|
}
|
||||||
|
if (!empty($filters['action'])) {
|
||||||
|
$q->where('action', $filters['action']);
|
||||||
|
}
|
||||||
|
if (!empty($filters['actor_id'])) {
|
||||||
|
$q->where('actor_id', (int)$filters['actor_id']);
|
||||||
|
}
|
||||||
|
if (!empty($filters['from'])) {
|
||||||
|
$q->where('created_at', '>=', $filters['from']);
|
||||||
|
}
|
||||||
|
if (!empty($filters['to'])) {
|
||||||
|
$q->where('created_at', '<=', $filters['to']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $q->orderBy($sort, $order)->paginate($size, ['*'], 'page', $page);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
app/Services/Audit/AuditLogger.php
Normal file
42
app/Services/Audit/AuditLogger.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Audit;
|
||||||
|
|
||||||
|
use App\Models\Audit\AuditLog;
|
||||||
|
|
||||||
|
class AuditLogger
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 감사 로그 기록
|
||||||
|
*/
|
||||||
|
public function log(
|
||||||
|
int $tenantId,
|
||||||
|
string $targetType,
|
||||||
|
?int $targetId,
|
||||||
|
string $action,
|
||||||
|
?array $before = null,
|
||||||
|
?array $after = null
|
||||||
|
): void {
|
||||||
|
try {
|
||||||
|
$request = request();
|
||||||
|
$actorId = optional(auth()->user())->id ?? null;
|
||||||
|
$ip = $request?->ip();
|
||||||
|
$ua = $request?->userAgent();
|
||||||
|
|
||||||
|
AuditLog::create([
|
||||||
|
'tenant_id' => $tenantId,
|
||||||
|
'target_type' => $targetType,
|
||||||
|
'target_id' => $targetId,
|
||||||
|
'action' => $action,
|
||||||
|
'before' => $before,
|
||||||
|
'after' => $after,
|
||||||
|
'actor_id' => $actorId,
|
||||||
|
'ip' => $ip,
|
||||||
|
'ua' => $ua,
|
||||||
|
'created_at' => now(),
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// 감사 로그 실패는 업무 흐름을 방해하지 않음
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,6 +51,9 @@ public function upsertTemplate(int $modelVersionId, string $name = 'Main', bool
|
|||||||
->where('name', $name)
|
->where('name', $name)
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
|
$action = 'created';
|
||||||
|
$before = null;
|
||||||
|
|
||||||
if (!$tpl) {
|
if (!$tpl) {
|
||||||
$tpl = BomTemplate::create([
|
$tpl = BomTemplate::create([
|
||||||
'tenant_id' => $tenantId,
|
'tenant_id' => $tenantId,
|
||||||
@@ -60,6 +63,8 @@ public function upsertTemplate(int $modelVersionId, string $name = 'Main', bool
|
|||||||
'notes' => $notes,
|
'notes' => $notes,
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
|
$action = 'updated';
|
||||||
|
$before = $tpl->toArray();
|
||||||
$tpl->fill(['is_primary' => $isPrimary, 'notes' => $notes])->save();
|
$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]);
|
->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;
|
return $tpl;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -99,6 +114,7 @@ public function updateTemplate(int $templateId, array $data): BomTemplate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$before = $tpl->toArray();
|
||||||
$tpl->fill($data)->save();
|
$tpl->fill($data)->save();
|
||||||
|
|
||||||
if (array_key_exists('is_primary', $data) && $data['is_primary']) {
|
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]);
|
->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;
|
return $tpl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +151,18 @@ public function deleteTemplate(int $templateId): void
|
|||||||
throw new NotFoundHttpException(__('error.not_found'));
|
throw new NotFoundHttpException(__('error.not_found'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$before = $tpl->toArray();
|
||||||
$tpl->delete();
|
$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)) {
|
if (!empty($payloads)) {
|
||||||
BomTemplateItem::insert($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,
|
'left_template_id' => $left->id,
|
||||||
'right_template_id' => $right->id,
|
'right_template_id' => $right->id,
|
||||||
'summary' => [
|
'summary' => [
|
||||||
@@ -321,6 +378,19 @@ public function diffTemplates(int $leftTemplateId, int $rightTemplateId): array
|
|||||||
'removed' => $removed,
|
'removed' => $removed,
|
||||||
'changed' => $changed,
|
'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]);
|
->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;
|
return $dest;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,10 +95,25 @@ public function release(int $versionId): ModelVersion
|
|||||||
// 릴리즈 전 유효성 검사
|
// 릴리즈 전 유효성 검사
|
||||||
app(\App\Services\Design\BomTemplateService::class)->validateForRelease($mv->id);
|
app(\App\Services\Design\BomTemplateService::class)->validateForRelease($mv->id);
|
||||||
|
|
||||||
|
$before = [
|
||||||
|
'status' => $mv->status,
|
||||||
|
'effective_from' => $mv->effective_from ? $mv->effective_from->toISOString() : null,
|
||||||
|
];
|
||||||
|
|
||||||
$mv->status = 'RELEASED';
|
$mv->status = 'RELEASED';
|
||||||
$mv->effective_from = $mv->effective_from ?? now();
|
$mv->effective_from = $mv->effective_from ?? now();
|
||||||
$mv->save();
|
$mv->save();
|
||||||
|
|
||||||
|
// 감사 로그
|
||||||
|
app(\App\Services\Audit\AuditLogger::class)->log(
|
||||||
|
tenantId: $tenantId,
|
||||||
|
targetType: 'model_version',
|
||||||
|
targetId: $mv->id,
|
||||||
|
action: 'released',
|
||||||
|
before: $before,
|
||||||
|
after: ['status' => $mv->status, 'effective_from' => $mv->effective_from->toISOString()]
|
||||||
|
);
|
||||||
|
|
||||||
return $mv;
|
return $mv;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
54
app/Swagger/v1/AuditLogApi.php
Normal file
54
app/Swagger/v1/AuditLogApi.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Swagger\v1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Tag(
|
||||||
|
* name="Design Audit",
|
||||||
|
* description="Design-time audit logs"
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class AuditLogApi
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/design/audit-logs",
|
||||||
|
* tags={"Design Audit"},
|
||||||
|
* summary="List audit logs",
|
||||||
|
* security={{"ApiKeyAuth": {}, "BearerAuth": {}}},
|
||||||
|
* @OA\Parameter(name="page", in="query", @OA\Schema(type="integer", minimum=1)),
|
||||||
|
* @OA\Parameter(name="size", in="query", @OA\Schema(type="integer", minimum=1, maximum=200)),
|
||||||
|
* @OA\Parameter(name="target_type", in="query", @OA\Schema(type="string")),
|
||||||
|
* @OA\Parameter(name="target_id", in="query", @OA\Schema(type="integer")),
|
||||||
|
* @OA\Parameter(name="action", in="query", @OA\Schema(type="string")),
|
||||||
|
* @OA\Parameter(name="actor_id", in="query", @OA\Schema(type="integer")),
|
||||||
|
* @OA\Parameter(name="from", in="query", @OA\Schema(type="string", format="date-time")),
|
||||||
|
* @OA\Parameter(name="to", in="query", @OA\Schema(type="string", format="date-time")),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="List",
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* type="object",
|
||||||
|
* @OA\Property(property="success", type="boolean"),
|
||||||
|
* @OA\Property(property="message", type="string"),
|
||||||
|
* @OA\Property(property="data", type="object",
|
||||||
|
* @OA\Property(property="current_page", type="integer"),
|
||||||
|
* @OA\Property(property="data", type="array", @OA\Items(type="object",
|
||||||
|
* @OA\Property(property="tenant_id", type="integer"),
|
||||||
|
* @OA\Property(property="target_type", type="string"),
|
||||||
|
* @OA\Property(property="target_id", type="integer", nullable=true),
|
||||||
|
* @OA\Property(property="action", type="string"),
|
||||||
|
* @OA\Property(property="before", type="object", nullable=true),
|
||||||
|
* @OA\Property(property="after", type="object", nullable=true),
|
||||||
|
* @OA\Property(property="actor_id", type="integer", nullable=true),
|
||||||
|
* @OA\Property(property="ip", type="string", nullable=true),
|
||||||
|
* @OA\Property(property="ua", type="string", nullable=true),
|
||||||
|
* @OA\Property(property="created_at", type="string", format="date-time")
|
||||||
|
* ))
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function docs() {}
|
||||||
|
}
|
||||||
9
config/audit.php
Normal file
9
config/audit.php
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
// 감사 로그 보관기간(일)
|
||||||
|
'retention_days' => env('AUDIT_RETENTION_DAYS', 395), // 기본 13개월
|
||||||
|
|
||||||
|
// 조회(diff) 로그 기록 여부(기본 비활성). 개발/QA에서만 활성화 권장
|
||||||
|
'log_reads' => env('AUDIT_LOG_READS', false),
|
||||||
|
];
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration {
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('audit_logs', function (Blueprint $table) {
|
||||||
|
$table->bigIncrements('id');
|
||||||
|
$table->unsignedBigInteger('tenant_id')->comment('테넌트ID');
|
||||||
|
$table->string('target_type', 100)->comment('리소스 타입 예: model_version, bom_template');
|
||||||
|
$table->unsignedBigInteger('target_id')->nullable()->comment('리소스 ID');
|
||||||
|
$table->string('action', 50)->comment('액션: created/updated/deleted/released/cloned/items_replaced/diff_viewed');
|
||||||
|
$table->json('before')->nullable()->comment('변경 전 스냅샷');
|
||||||
|
$table->json('after')->nullable()->comment('변경 후 스냅샷');
|
||||||
|
$table->unsignedBigInteger('actor_id')->nullable()->comment('사용자ID(옵션)');
|
||||||
|
$table->string('ip', 45)->nullable()->comment('요청 IP');
|
||||||
|
$table->string('ua', 255)->nullable()->comment('User-Agent');
|
||||||
|
$table->timestamp('created_at')->useCurrent();
|
||||||
|
|
||||||
|
$table->index(['tenant_id', 'target_type', 'target_id', 'created_at'], 'ix_audit_tenant_target_created');
|
||||||
|
$table->index(['tenant_id', 'actor_id', 'created_at'], 'ix_audit_tenant_actor_created');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('audit_logs');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
use App\Http\Controllers\Api\V1\Design\DesignModelController as DesignModelController;
|
use App\Http\Controllers\Api\V1\Design\DesignModelController as DesignModelController;
|
||||||
use App\Http\Controllers\Api\V1\Design\ModelVersionController as DesignModelVersionController;
|
use App\Http\Controllers\Api\V1\Design\ModelVersionController as DesignModelVersionController;
|
||||||
use App\Http\Controllers\Api\V1\Design\BomTemplateController as DesignBomTemplateController;
|
use App\Http\Controllers\Api\V1\Design\BomTemplateController as DesignBomTemplateController;
|
||||||
|
use App\Http\Controllers\Api\V1\Design\AuditLogController as DesignAuditLogController;
|
||||||
|
|
||||||
// error test
|
// error test
|
||||||
Route::get('/test-error', function () {
|
Route::get('/test-error', function () {
|
||||||
@@ -356,6 +357,9 @@
|
|||||||
Route::put ('/bom-templates/{templateId}/items', [DesignBomTemplateController::class, 'replaceItems'])->name('v1.design.bom.templates.items.replace');
|
Route::put ('/bom-templates/{templateId}/items', [DesignBomTemplateController::class, 'replaceItems'])->name('v1.design.bom.templates.items.replace');
|
||||||
Route::get ('/bom-templates/{templateId}/diff', [DesignBomTemplateController::class, 'diff'])->name('v1.design.bom.templates.diff');
|
Route::get ('/bom-templates/{templateId}/diff', [DesignBomTemplateController::class, 'diff'])->name('v1.design.bom.templates.diff');
|
||||||
Route::post ('/bom-templates/{templateId}/clone', [DesignBomTemplateController::class, 'cloneTemplate'])->name('v1.design.bom.templates.clone');
|
Route::post ('/bom-templates/{templateId}/clone', [DesignBomTemplateController::class, 'cloneTemplate'])->name('v1.design.bom.templates.clone');
|
||||||
|
|
||||||
|
// 감사 로그 조회
|
||||||
|
Route::get ('/audit-logs', [DesignAuditLogController::class, 'index'])->name('v1.design.audit-logs.index');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user