# DB 트리거 기반 데이터 변경 추적 시스템 계획 > **작성일**: 2026-02-07 > **목적**: 모든 경로(앱, 직접SQL, AI, phpMyAdmin 등)의 데이터 변경을 DB 레벨에서 추적하고 복구 가능하게 함 > **기준 문서**: `docs/specs/database-schema.md`, `api/app/Traits/Auditable.php`, `api/config/audit.php` > **상태**: 🔄 Phase 1-3 완료, Phase 4 핵심 완료 (4.4~4.6 옵션 잔여) --- ## 📍 현재 진행 상태 | 항목 | 내용 | |------|------| | **마지막 완료 작업** | Phase 4 핵심 (mng 대시보드 + 목록 + 상세 + 이력 + 롤백) | | **다음 작업** | Phase 4.4 트리거 관리 화면 (옵션) | | **진행률** | 15/16 (94%) - 핵심 기능 완료, 옵션 3개 잔여 | | **마지막 업데이트** | 2026-02-07 | --- ## 1. 개요 ### 1.1 배경 SAM 프로젝트에는 이미 Laravel `Auditable` trait 기반 감사 로그가 존재하지만, 이는 **Laravel Eloquent ORM을 통한 변경만 추적**한다. 다음 경로의 변경은 추적 불가: - AI(Claude 등)가 직접 실행하는 SQL 쿼리 - phpMyAdmin, DBeaver 등 DB 클라이언트에서의 직접 수정 - MySQL CLI에서의 직접 쿼리 - 다른 애플리케이션/스크립트에서의 DB 접근 - Laravel `DB::statement()` 등 Eloquent 우회 쿼리 **해결책**: MySQL 트리거를 사용하여 DB 엔진 레벨에서 모든 INSERT/UPDATE/DELETE를 포착한다. ### 1.2 기준 원칙 ``` +------------------------------------------------------------------+ | 계층 분리 (Layered Audit) | +------------------------------------------------------------------+ | Layer 1: Laravel Audit (기존 유지) | | - 비즈니스 액션 (released, cloned, items_replaced 등) | | - 사용자 컨텍스트 풍부 (IP, UA, 세션 정보) | | - 실패 시 비즈니스 로직 불영향 (try/catch) | +------------------------------------------------------------------+ | Layer 2: MySQL Trigger Audit (신규) | | - 모든 DML 포착 (직접 쿼리 포함, 누락 불가) | | - 컬럼 단위 old/new values JSON 저장 | | - 특정 레코드의 특정 시점으로 복원 가능 | +------------------------------------------------------------------+ ``` ### 1.3 변경 승인 정책 | 분류 | 예시 | 승인 | |------|------|------| | ✅ 즉시 가능 | 트리거 대상 테이블 목록 조정, 제외 컬럼 변경 | 불필요 | | ⚠️ 컨펌 필요 | 마이그레이션 실행, 트리거 생성/변경, 미들웨어 추가, 새 API 엔드포인트 | **필수** | | 🔴 금지 | 기존 audit_logs 테이블 구조 변경, 기존 Auditable trait 수정 | 별도 협의 | ### 1.4 준수 규칙 - `docs/quickstart/quick-start.md` - 빠른 시작 가이드 - `docs/standards/quality-checklist.md` - 품질 체크리스트 - `docs/specs/database-schema.md` - DB 스키마 - `docs/standards/api-rules.md` - API 규칙 (Audit Logging 섹션) --- ## 2. 대상 범위 ### 2.1 Phase 1: DB 기반 구축 | # | 작업 항목 | 상태 | 비고 | |---|----------|:----:|------| | 1.1 | trigger_audit_logs 테이블 마이그레이션 (파티셔닝 포함) | ✅ | 15개 파티션, 3개 인덱스 | | 1.2 | 트리거 대상 테이블 선정 및 확정 | ✅ | 제외 11개 외 전체 적용 | | 1.3 | 트리거 자동 생성 (PHP 기반, SP 불가) | ✅ | MySQL CREATE TRIGGER는 PREPARE 미지원 → PHP 마이그레이션으로 전환 | | 1.4 | 대상 테이블별 트리거 생성 | ✅ | 789개 트리거 (263 테이블 × 3) | | 1.5 | 세션 변수 설정 미들웨어 (Laravel) | ✅ | @sam_actor_id, @sam_session_info | ### 2.2 Phase 2: 복구 메커니즘 | # | 작업 항목 | 상태 | 비고 | |---|----------|:----:|------| | 2.1 | TriggerAuditLog 모델 | ✅ | casts, scopes, changed_columns accessor | | 2.2 | AuditRollbackService 구현 | ✅ | rollback SQL 생성 + 실행 + getRecordStateAt | | 2.3 | Trigger Audit 조회 API | ✅ | 6개 엔드포인트 (index, show, stats, history, rollback-preview, rollback) | | 2.4 | Rollback API 엔드포인트 | ✅ | POST /api/v1/trigger-audit-logs/{id}/rollback + confirm 필수 | ### 2.3 Phase 3: 관리 도구 | # | 작업 항목 | 상태 | 비고 | |---|----------|:----:|------| | 3.1 | 통합 조회 뷰 (v_unified_audit) | ✅ | APP 3,108건 + TRIGGER 2,649건 통합, COLLATE 해결 | | 3.2 | 파티션 자동 관리 (artisan 커맨드) | ✅ | audit:partitions --add-months --retention-months --drop --dry-run | | 3.3 | 트리거 재생성 artisan 커맨드 | ✅ | audit:triggers --table --drop-only --dry-run | ### 2.4 Phase 4: 관리자 대시보드 (mng) | # | 작업 항목 | 상태 | 비고 | |---|----------|:----:|------| | 4.1 | 변경 이력 목록 화면 (index) | ✅ | 통계카드+필터+목록+파티션현황+트리거수, 페이지네이션 | | 4.2 | 레코드 상세 변경 이력 (show + history) | ✅ | diff 뷰(old/new 비교, 변경 컬럼 하이라이트) + 레코드 타임라인 | | 4.3 | 복구 기능 UI (rollback-preview) | ✅ | SQL 미리보기, 확인 체크박스+confirm, @disable_audit_trigger | | 4.4 | 트리거 관리 화면 | ⏭️ | 옵션 - artisan audit:triggers 커맨드로 CLI 관리 가능 | | 4.5 | 대시보드 통계 | ✅ | index에 통합 (전체/오늘/DML별 통계, 상위 테이블, 파티션, 저장소) | | 4.6 | 보관 정책 설정 | ⏭️ | 옵션 - artisan audit:partitions 커맨드로 CLI 관리 가능 | --- ## 3. 작업 절차 ### 3.1 아키텍처 다이어그램 ``` [사용자/AI/phpMyAdmin/스크립트] │ ▼ ┌─────────┐ │ MySQL │ │ Engine │ └────┬────┘ │ DML (INSERT/UPDATE/DELETE) ▼ ┌─────────────────────────┐ │ 대상 테이블 │ │ (제외 목록 외 전체 │ │ 약 207개) │ └────┬────────────────────┘ │ AFTER 트리거 발동 ▼ ┌─────────────────────────┐ │ trigger_audit_logs │ │ (파티셔닝, 13개월 보관) │ │ - table_name │ │ - row_id │ │ - dml_type │ │ - old_values (JSON) │ │ - new_values (JSON) │ │ - tenant_id │ │ - actor_id │ ← @sam_actor_id 세션변수 │ - session_info │ ← @sam_session_info 세션변수 │ - db_user │ ← CURRENT_USER() │ - created_at │ └─────────────────────────┘ │ ▼ ┌─────────────────────────┐ │ AuditRollbackService │ │ (Laravel) │ │ - 이력 조회 │ │ - Rollback SQL 생성 │ │ - 특정 시점 복원 │ └─────────────────────────┘ ``` ### 3.2 트리거 대상 테이블 #### 적용 방침: 전체 적용 (제외 목록 방식) 로컬 개발 환경에서 1인 사용이므로, **제외 대상을 제외한 모든 테이블에 트리거를 적용**한다. 운영 환경 전환 시 필요에 따라 대상을 축소할 수 있다. SP(`sp_create_audit_triggers`)가 `INFORMATION_SCHEMA.TABLES`에서 samdb의 전체 테이블을 읽고, 제외 목록에 없는 모든 테이블에 자동으로 트리거를 생성한다. #### 제외 대상 (트리거 미적용) | 테이블 패턴 | 사유 | |-------------|------| | `audit_logs` | 감사 로그 자체 (순환 방지) | | `trigger_audit_logs` | 트리거 감사 자체 (순환 방지) | | `personal_access_tokens` | Sanctum 토큰 (대량 생성/삭제, 보안 데이터) | | `sessions` | 세션 데이터 (빈번한 갱신) | | `cache`, `cache_locks` | 캐시 데이터 | | `jobs`, `job_batches` | 큐 작업 | | `failed_jobs` | 실패 큐 | | `migrations` | 마이그레이션 기록 | | `password_reset_tokens` | 비밀번호 리셋 토큰 | | `telescope_*` | 디버그 도구 (있는 경우) | > **예상**: samdb 약 219개 테이블 - 제외 약 12개 = **약 207개 테이블 × 3 트리거 = 약 621개 트리거** > > SP가 `INFORMATION_SCHEMA`에서 동적으로 테이블을 읽으므로, 테이블이 추가/삭제되면 > `artisan audit:regenerate-triggers` 명령으로 트리거를 재생성하면 된다. ### 3.3 trigger_audit_logs 테이블 구조 ```sql CREATE TABLE trigger_audit_logs ( id BIGINT UNSIGNED AUTO_INCREMENT, table_name VARCHAR(64) NOT NULL COMMENT '변경된 테이블명', row_id VARCHAR(64) NOT NULL COMMENT '변경된 레코드 PK (문자열 지원)', dml_type ENUM('INSERT','UPDATE','DELETE') NOT NULL COMMENT 'DML 유형', old_values JSON DEFAULT NULL COMMENT '변경 전 값 (INSERT시 NULL)', new_values JSON DEFAULT NULL COMMENT '변경 후 값 (DELETE시 NULL)', changed_columns JSON DEFAULT NULL COMMENT 'UPDATE시 변경된 컬럼 목록', tenant_id BIGINT UNSIGNED DEFAULT NULL COMMENT '테넌트 ID', actor_id BIGINT UNSIGNED DEFAULT NULL COMMENT '사용자 ID (세션변수)', session_info VARCHAR(500) DEFAULT NULL COMMENT '세션 정보 JSON (IP, UA 등)', db_user VARCHAR(100) DEFAULT NULL COMMENT 'CURRENT_USER()', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '변경 시각', PRIMARY KEY (id, created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='DB 트리거 기반 데이터 변경 추적' PARTITION BY RANGE (UNIX_TIMESTAMP(created_at)) ( PARTITION p202601 VALUES LESS THAN (UNIX_TIMESTAMP('2026-02-01')), PARTITION p202602 VALUES LESS THAN (UNIX_TIMESTAMP('2026-03-01')), PARTITION p202603 VALUES LESS THAN (UNIX_TIMESTAMP('2026-04-01')), PARTITION p202604 VALUES LESS THAN (UNIX_TIMESTAMP('2026-05-01')), PARTITION p202605 VALUES LESS THAN (UNIX_TIMESTAMP('2026-06-01')), PARTITION p202606 VALUES LESS THAN (UNIX_TIMESTAMP('2026-07-01')), PARTITION p202607 VALUES LESS THAN (UNIX_TIMESTAMP('2026-08-01')), PARTITION p202608 VALUES LESS THAN (UNIX_TIMESTAMP('2026-09-01')), PARTITION p202609 VALUES LESS THAN (UNIX_TIMESTAMP('2026-10-01')), PARTITION p202610 VALUES LESS THAN (UNIX_TIMESTAMP('2026-11-01')), PARTITION p202611 VALUES LESS THAN (UNIX_TIMESTAMP('2026-12-01')), PARTITION p202612 VALUES LESS THAN (UNIX_TIMESTAMP('2027-01-01')), PARTITION p202701 VALUES LESS THAN (UNIX_TIMESTAMP('2027-02-01')), PARTITION p202702 VALUES LESS THAN (UNIX_TIMESTAMP('2027-03-01')), PARTITION p_future VALUES LESS THAN MAXVALUE ); -- 조회 성능 인덱스 CREATE INDEX ix_trig_table_row_created ON trigger_audit_logs (table_name, row_id, created_at); CREATE INDEX ix_trig_tenant_created ON trigger_audit_logs (tenant_id, created_at); ``` ### 3.4 트리거 자동 생성 Stored Procedure ```sql -- 특정 테이블에 대해 AFTER INSERT/UPDATE/DELETE 트리거 3개를 자동 생성 -- INFORMATION_SCHEMA.COLUMNS에서 컬럼 목록을 읽어 JSON_OBJECT 구문 자동 조립 CALL sp_create_audit_triggers('products'); -- → trg_products_ai (AFTER INSERT) -- → trg_products_au (AFTER UPDATE) -- → trg_products_ad (AFTER DELETE) ``` **SP 핵심 로직:** 1. `INFORMATION_SCHEMA.COLUMNS`에서 대상 테이블의 컬럼 목록 조회 2. 제외 컬럼 필터링 (`created_at`, `updated_at`, `deleted_at`, `remember_token` 등) 3. `JSON_OBJECT('col1', NEW.col1, 'col2', NEW.col2, ...)` 구문 자동 조립 4. UPDATE 트리거: 컬럼별 `OLD.col <> NEW.col` 비교 → changed_columns 배열 생성 5. 비활성화 플래그 체크 (`@disable_audit_trigger`) 6. `PREPARE + EXECUTE`로 트리거 DDL 실행 ### 3.5 세션 변수 미들웨어 ```php // app/Http/Middleware/SetAuditSessionVariables.php class SetAuditSessionVariables { public function handle($request, $next) { if (auth()->check()) { DB::statement("SET @sam_actor_id = ?", [auth()->id()]); DB::statement("SET @sam_session_info = ?", [ json_encode([ 'ip' => $request->ip(), 'ua' => substr($request->userAgent(), 0, 255), 'route' => $request->route()?->getName(), ]) ]); } return $next($request); } } ``` ### 3.6 복구 서비스 ```php // app/Services/Audit/AuditRollbackService.php class AuditRollbackService { // 특정 audit 레코드에 대한 역방향 SQL 생성 public function generateRollbackSQL(int $auditId): string; // 실제 복구 실행 (트랜잭션 내에서) public function executeRollback(int $auditId): bool; // 특정 레코드의 특정 시점 상태 조회 public function getRecordStateAt(string $table, string $rowId, Carbon $at): ?array; // 특정 레코드의 변경 이력 조회 public function getRecordHistory(string $table, string $rowId): Collection; } ``` **복구 로직:** | 원본 DML | 복구 SQL | |----------|---------| | INSERT | `DELETE FROM {table} WHERE id = {row_id}` | | UPDATE | `UPDATE {table} SET {old_values 각 컬럼} WHERE id = {row_id}` | | DELETE | `INSERT INTO {table} ({old_values 컬럼}) VALUES ({old_values 값})` | --- ## 4. 상세 작업 내용 > 각 Phase 진행 후 이 섹션에 상세 내용 추가 ### 4.1 Phase 1: DB 기반 구축 - **상태**: ⏳ 대기 - **예상 파일**: - `api/database/migrations/YYYY_MM_DD_HHMMSS_create_trigger_audit_logs_table.php` - `api/database/migrations/YYYY_MM_DD_HHMMSS_create_audit_trigger_stored_procedures.php` - `api/database/migrations/YYYY_MM_DD_HHMMSS_create_audit_triggers_for_tables.php` - `api/app/Http/Middleware/SetAuditSessionVariables.php` ### 4.2 Phase 2: 복구 메커니즘 - **상태**: ⏳ 대기 - **예상 파일**: - `api/app/Models/Audit/TriggerAuditLog.php` - `api/app/Services/Audit/AuditRollbackService.php` - `api/app/Http/Controllers/Api/V1/Audit/TriggerAuditLogController.php` - `api/app/Http/Requests/Audit/TriggerAuditLogIndexRequest.php` - `api/app/Http/Requests/Audit/TriggerAuditRollbackRequest.php` - `api/app/Swagger/v1/TriggerAuditLogApi.php` ### 4.3 Phase 3: 관리 도구 - **상태**: ⏳ 대기 - **예상 파일**: - `api/database/migrations/YYYY_MM_DD_HHMMSS_create_unified_audit_view.php` - `api/app/Console/Commands/ManageAuditPartitions.php` - `api/app/Console/Commands/RegenerateAuditTriggers.php` ### 4.4 Phase 4: 관리자 대시보드 (mng) - **상태**: ⏳ 대기 - **예상 파일**: - `mng/app/Http/Controllers/Admin/TriggerAuditController.php` - `mng/resources/views/admin/trigger-audit/index.blade.php` (이력 목록) - `mng/resources/views/admin/trigger-audit/show.blade.php` (상세 diff 뷰) - `mng/resources/views/admin/trigger-audit/dashboard.blade.php` (대시보드 통계) - `mng/resources/views/admin/trigger-audit/triggers.blade.php` (트리거 관리) - `mng/resources/views/admin/trigger-audit/settings.blade.php` (보관 정책) - `mng/app/Services/TriggerAuditDashboardService.php` --- ## 5. 컨펌 대기 목록 | # | 항목 | 변경 내용 | 영향 범위 | 상태 | |---|------|----------|----------|------| | 1 | 트리거 대상 | 제외 목록 외 전체 (약 207개) 적용 | database | ✅ 확정 | | 2 | 성능 영향 | 로컬 1인 사용, 제한 없음 | database | ✅ 확정 | | 3 | Phase 4 범위 | 풀 관리 대시보드 (조회/복구/트리거관리/통계/정책) | mng | ✅ 확정 | --- ## 6. 변경 이력 | 날짜 | 항목 | 변경 내용 | 파일 | 승인 | |------|------|----------|------|------| | 2026-02-07 | 계획 | 문서 초안 작성 | - | - | | 2026-02-07 | 수정 | 피드백 반영: 전체 테이블 적용, Phase 4 대시보드 추가 | - | ✅ | | 2026-02-07 | Phase 1 | DB 기반 구축 완료. SP→PHP 전환, 789 트리거 생성, my.cnf 설정 추가 | api/database/migrations/2026_02_07_*, api/app/Http/Middleware/SetAuditSessionVariables.php, docker/mysql/my.cnf | ✅ | | 2026-02-07 | Phase 2 | 복구 메커니즘 API 완료. 모델/서비스/컨트롤러/라우트 6개 엔드포인트 | TriggerAuditLog.php, TriggerAuditLogService.php, AuditRollbackService.php, TriggerAuditLogController.php, audit.php(route) | ✅ | | 2026-02-07 | Phase 3 | 관리 도구 완료. 통합 뷰(collation 해결), 파티션 관리, 트리거 재생성 커맨드 | v_unified_audit VIEW, ManageAuditPartitions.php, RegenerateAuditTriggers.php | ✅ | --- ## 7. 참고 문서 - **빠른 시작**: `docs/quickstart/quick-start.md` - **품질 체크리스트**: `docs/standards/quality-checklist.md` - **DB 스키마**: `docs/specs/database-schema.md` - **API 규칙**: `docs/standards/api-rules.md` (Audit Logging 섹션) - **기존 Auditable**: `api/app/Traits/Auditable.php` - **기존 audit 설정**: `api/config/audit.php` - **기존 audit 마이그레이션**: `api/database/migrations/2025_09_11_000100_create_audit_logs_table.php` ### 외부 참고자료 - [MySQL 8.0 Trigger Syntax](https://dev.mysql.com/doc/refman/8.0/en/trigger-syntax.html) - [MySQL 8.0 Partitioning](https://dev.mysql.com/doc/refman/8.0/en/partitioning.html) - [Percona - MySQL Trigger Performance](https://www.percona.com/blog/why-mysql-stored-procedures-functions-triggers-bad-performance/) --- ## 8. 리스크 및 대응 방안 | 리스크 | 영향 | 대응 | |--------|------|------|| 트리거 성능 오버헤드 (INSERT 약 40-50%) | 쓰기 성능 저하 | 로컬 1인 사용 환경이므로 무관. 운영 전환 시 대상 축소 가능. Bulk 작업 시 `@disable_audit_trigger=1` | | 트리거 실패 시 원본 DML도 롤백 | 비즈니스 중단 | 트리거 로직 최소화, audit 테이블 구조 안정적 유지 | | 스키마 변경 시 트리거 유지보수 | 누락 위험 | SP 기반 자동 생성 → `artisan audit:regenerate-triggers` | | 저장 용량 증가 | 디스크 사용량 | 월별 파티셔닝 + 13개월 자동 삭제 | | 세션 변수 미설정 (CLI, Queue) | actor_id NULL | NULL 허용, db_user로 보완 추적 | --- ## 9. 검증 결과 > 작업 완료 후 이 섹션에 검증 결과 추가 ### 9.1 테스트 케이스 | 입력값 | 예상 결과 | 실제 결과 | 상태 | |--------|----------|----------|------| | Laravel에서 Product 생성 | audit_logs + trigger_audit_logs 모두 기록 | | ⏳ | | 직접 SQL로 Product UPDATE | trigger_audit_logs에만 기록 | | ⏳ | | phpMyAdmin에서 DELETE | trigger_audit_logs에 기록 (actor_id=NULL, db_user 기록) | | ⏳ | | Bulk INSERT 10,000건 (트리거 활성) | trigger_audit_logs에 10,000건 기록, 성능 측정 | | ⏳ | | Bulk INSERT 10,000건 (트리거 비활성) | trigger_audit_logs 기록 없음, 기본 성능 | | ⏳ | | UPDATE 후 rollback API 호출 | old_values로 복원됨 | | ⏳ | | DELETE 후 rollback API 호출 | 삭제된 레코드 복원됨 | | ⏳ | | INSERT 후 rollback API 호출 | 삽입된 레코드 삭제됨 | | ⏳ | | 13개월 이전 파티션 삭제 | 해당 파티션 DROP, 데이터 제거 | | ⏳ | ### 9.2 성공 기준 | 기준 | 달성 | 비고 | |------|------|------| | 직접 SQL 변경이 trigger_audit_logs에 기록됨 | ⏳ | | | old_values/new_values JSON이 정확히 저장됨 | ⏳ | | | 특정 레코드의 특정 시점 복원이 가능함 | ⏳ | | | 파티셔닝이 정상 작동함 | ⏳ | | | 기존 Laravel audit 시스템에 영향 없음 | ⏳ | | | 트리거 비활성화 플래그가 정상 동작함 | ⏳ | | | mng 대시보드에서 이력 조회/필터링 가능 | ⏳ | | | mng에서 특정 변경 복구(rollback) 가능 | ⏳ | | | mng에서 테이블별 트리거 ON/OFF 가능 | ⏳ | | --- ## 10. 자기완결성 점검 결과 ### 10.1 체크리스트 검증 | # | 검증 항목 | 상태 | 비고 | |---|----------|:----:|------| | 1 | 작업 목적이 명확한가? | ✅ | 1.1 배경에 명시 | | 2 | 성공 기준이 정의되어 있는가? | ✅ | 9.2 성공 기준 | | 3 | 작업 범위가 구체적인가? | ✅ | 2.1~2.4 Phase별 작업 항목 | | 4 | 의존성이 명시되어 있는가? | ✅ | Phase 순서, 기존 시스템 참조 | | 5 | 참고 파일 경로가 정확한가? | ✅ | 7. 참고 문서 | | 6 | 단계별 절차가 실행 가능한가? | ✅ | 3.3~3.6 상세 구현 명세 | | 7 | 검증 방법이 명시되어 있는가? | ✅ | 9.1 테스트 케이스 | | 8 | 모호한 표현이 없는가? | ✅ | 구체적 수치/조건 명시 | ### 10.2 새 세션 시뮬레이션 테스트 | 질문 | 답변 가능 | 참조 섹션 | |------|:--------:|----------| | Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | | Q2. 어디서부터 시작해야 하는가? | ✅ | 2.1 Phase 1 → 1.1 테이블 생성 | | Q3. 어떤 파일을 수정해야 하는가? | ✅ | 4.1~4.4 예상 파일 목록 | | Q4. 작업 완료 확인 방법은? | ✅ | 9.1 테스트 케이스, 9.2 성공 기준 | | Q5. 막혔을 때 참고 문서는? | ✅ | 7. 참고 문서 | **결과**: 5/5 통과 → ✅ 자기완결성 확보 --- ## 부록 A: 환경 정보 ### A.1 프로젝트 구조 ``` SAM/ ← 프로젝트 루트 ├── api/ ← Laravel 12 REST API (독립 git) │ ├── app/ │ │ ├── Http/ │ │ │ ├── Controllers/Api/V1/ ← API 컨트롤러 │ │ │ ├── Middleware/ ← 미들웨어 │ │ │ └── Requests/ ← FormRequest │ │ ├── Models/Audit/ ← 감사 모델 (AuditLog.php) │ │ ├── Services/Audit/ ← 감사 서비스 (AuditLogger, AuditLogService) │ │ ├── Traits/ ← Auditable.php, BelongsToTenant.php │ │ ├── Console/Commands/ ← Artisan 커맨드 │ │ └── Swagger/v1/ ← Swagger 문서 │ ├── config/audit.php ← 감사 설정 │ ├── database/migrations/ ← 마이그레이션 │ ├── routes/ │ │ ├── api.php ← 메인 라우트 (v1 prefix → 도메인별 분리) │ │ └── api/v1/ ← 도메인별 라우트 파일 │ └── bootstrap/app.php ← Laravel 12 미들웨어 등록 ├── mng/ ← Laravel 12 관리자 패널 (독립 git) │ ├── app/Http/Controllers/ ← Blade 컨트롤러 │ ├── resources/views/ ← Blade 뷰 (Tailwind + Alpine.js + HTMX) │ │ └── layouts/app.blade.php ← 메인 레이아웃 │ └── routes/web.php ← 웹 라우트 (auth 미들웨어 그룹) ├── react/ ← Next.js 15 프론트엔드 └── docs/dev_plans/ ← 이 문서 ``` ### A.2 DB 접속 정보 ``` 엔진: MySQL 8.0 Docker 컨테이너: sam-mysql-1 데이터베이스: samdb (주), sam_stat (통계) 호스트: 127.0.0.1 (로컬) / sam-mysql-1 (Docker 내부) 포트: 3306 사용자: samuser / sampass (일반), root / root (관리자) 문자셋: utf8mb4 / utf8mb4_unicode_ci 타임존: Asia/Seoul ``` ### A.3 주요 명령어 ```bash # Docker cd /Users/kent/Works/@KD_SAM/SAM docker compose up -d mysql # API 마이그레이션 cd api && php artisan migrate cd api && php artisan migrate:status # MySQL 직접 접속 docker exec -it sam-mysql-1 mysql -u root -proot samdb # MNG Vite 빌드 cd mng && npm run dev ``` --- ## 부록 B: 기존 감사 시스템 코드 (수정 금지, 참조용) ### B.1 Auditable Trait (`api/app/Traits/Auditable.php`) ```php isFillable('created_by') && ! $model->created_by) { $model->created_by = $actorId; } if ($model->isFillable('updated_by') && ! $model->updated_by) { $model->updated_by = $actorId; } } }); static::updating(function ($model) { $actorId = static::resolveActorId(); if ($actorId && $model->isFillable('updated_by')) { $model->updated_by = $actorId; } }); static::deleting(function ($model) { $actorId = static::resolveActorId(); if ($actorId && $model->isFillable('deleted_by')) { $model->deleted_by = $actorId; $model->saveQuietly(); } }); static::created(function ($model) { $model->logAuditEvent('created', null, $model->toAuditSnapshot()); }); static::updated(function ($model) { $dirty = $model->getChanges(); $excluded = $model->getAuditExcludedFields(); $changed = array_diff_key($dirty, array_flip($excluded)); if (empty($changed)) return; $before = []; $after = []; foreach ($changed as $key => $value) { $before[$key] = $model->getOriginal($key); $after[$key] = $value; } $model->logAuditEvent('updated', $before, $after); }); static::deleted(function ($model) { $model->logAuditEvent('deleted', $model->toAuditSnapshot(), null); }); } public function getAuditExcludedFields(): array { $defaults = ['created_at','updated_at','deleted_at','created_by','updated_by','deleted_by']; $custom = property_exists($this, 'auditExclude') ? $this->auditExclude : []; return array_merge($defaults, $custom); } public function getAuditTargetType(): string { return Str::snake(class_basename(static::class)); } protected function toAuditSnapshot(): array { return array_diff_key($this->attributesToArray(), array_flip($this->getAuditExcludedFields())); } protected function logAuditEvent(string $action, ?array $before, ?array $after): void { try { $tenantId = $this->tenant_id ?? null; if (! $tenantId) return; $request = request(); AuditLog::create([ 'tenant_id' => $tenantId, 'target_type' => $this->getAuditTargetType(), 'target_id' => $this->getKey(), 'action' => $action, 'before' => $before, 'after' => $after, 'actor_id' => static::resolveActorId(), 'ip' => $request?->ip(), 'ua' => $request?->userAgent(), 'created_at' => now(), ]); } catch (\Throwable $e) { // 감사 로그 실패는 업무 흐름을 방해하지 않음 } } protected static function resolveActorId(): ?int { return auth()->id(); } } ``` ### B.2 AuditLog 모델 (`api/app/Models/Audit/AuditLog.php`) ```php 'array', 'after' => 'array', 'created_at' => 'datetime', ]; } ``` ### B.3 AuditLogService (`api/app/Services/Audit/AuditLogService.php`) ```php tenantId(); $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']); $sort = $filters['sort'] ?? 'created_at'; $order = $filters['order'] ?? 'desc'; $size = (int) ($filters['size'] ?? 20); return $q->orderBy($sort, $order)->paginate($size); } } ``` ### B.4 Audit Config (`api/config/audit.php`) ```php env('AUDIT_RETENTION_DAYS', 395), // 13개월 'log_reads' => env('AUDIT_LOG_READS', false), ]; ``` ### B.5 API 컨트롤러 패턴 (`api/app/Http/Controllers/Api/V1/Design/AuditLogController.php`) ```php service->paginate($request->validated()); }, __('message.fetched')); } } ``` ### B.6 API Kernel (`api/app/Http/Kernel.php`) ```php [], 'api' => [], ]; protected $routeMiddleware = []; } ``` > **참고**: Laravel 12에서 미들웨어 추가 시 `bootstrap/app.php`의 `->withMiddleware()` 또는 > `Kernel.php`의 `$middleware` / `$middlewareGroups`에 등록한다. ### B.7 API 라우트 패턴 (`api/routes/api.php`) ```php // 도메인별 분리 구조 Route::prefix('v1')->group(function () { require __DIR__.'/api/v1/auth.php'; require __DIR__.'/api/v1/design.php'; // ... 기타 도메인 }); // design.php 내 감사 로그 라우트 예시 Route::prefix('design')->group(function () { Route::prefix('audit-logs')->group(function () { Route::get('', [DesignAuditLogController::class, 'index']); Route::get('/{id}', [DesignAuditLogController::class, 'show'])->whereNumber('id'); }); }); ``` ### B.8 Artisan 커맨드 패턴 (예: `TenantsBootstrap.php`) ```php
| 컬럼 |
|---|
| {{ $item->field }} |