# SAM 통계 시스템 (sam_stat DB) 설계 계획 > **작성일**: 2026-01-29 > **목적**: SAM ERP의 확장 가능한 통계 전용 데이터베이스(sam_stat) 설계 > **기준 문서**: `docs/specs/database-schema.md`, `docs/architecture/system-overview.md` > **상태**: ✅ 구현 완료 --- ## 📍 현재 진행 상태 | 항목 | 내용 | |------|------| | **마지막 완료 작업** | Phase 6: 문서화 및 마무리 완료 (Swagger, DB 스키마 문서, 계획 문서 완료 처리) | | **다음 작업** | ✅ 전체 완료 | | **진행률** | 6/6 Phase (100%) | | **마지막 업데이트** | 2026-01-30 | --- ## 0. 프로젝트 컨텍스트 (새 세션용) > **이 섹션은 새 세션에서 이 문서만으로 작업을 시작할 수 있도록 필요한 모든 컨텍스트를 포함한다.** ### 0.1 프로젝트 구조 ``` /Users/kent/Works/@KD_SAM/SAM/ ├── api/ ← 작업 대상 (Laravel 12 REST API, PHP 8.4+) │ ├── app/ │ │ ├── Console/Commands/ # Artisan 커맨드 (19개 존재) │ │ ├── Http/Controllers/Api/V1/ # API 컨트롤러 │ │ ├── Models/ # Eloquent 모델 (167개) │ │ │ ├── Stats/ # ← 새로 생성할 통계 모델 디렉토리 │ │ │ ├── Tenants/ # 테넌트 스코프 모델 (가장 많음) │ │ │ ├── Orders/ # 수주 관련 │ │ │ ├── Production/ # 생산 관련 │ │ │ └── ... │ │ └── Services/ # 비즈니스 로직 (Service-First 아키텍처) │ │ ├── Stats/ # ← 새로 생성할 통계 서비스 디렉토리 │ │ ├── DashboardService.php # 기존 대시보드 (355줄, 원본 DB 실시간 집계) │ │ ├── ReportService.php # 기존 보고서 (일일일보, 지출예상) │ │ ├── DailyReportService.php # 일일 보고서 (어음, 계좌, 요약) │ │ ├── AiReportService.php # AI 보고서 │ │ └── ... │ ├── config/ │ │ └── database.php # DB 연결 설정 (mysql, chandj 존재) │ ├── database/ │ │ └── migrations/ # 279개 마이그레이션 파일 │ ├── routes/ │ │ ├── console.php # 스케줄러 정의 (Laravel 12 방식) │ │ └── api/v1/ │ │ ├── common.php # dashboard, reports 라우트 │ │ ├── finance.php # daily-report 라우트 │ │ └── ... # 14개 라우트 파일 │ └── .env # 환경변수 ├── mng/ # 관리자 패널 (Plain Laravel + Blade/Tailwind) ├── react/ # Next.js 15 프론트엔드 ├── docker/ │ └── docker-compose.yml # Docker 설정 └── docs/ # 기술 문서 ├── specs/database-schema.md # DB 스키마 문서 ├── architecture/system-overview.md └── plans/ # 이 문서의 위치 ``` ### 0.2 현재 DB 환경 ``` # .env (api/) DB_CONNECTION=mysql DB_HOST=127.0.0.1 # Docker 내부: sam-mysql-1 DB_PORT=3306 DB_DATABASE=samdb # ← 원본 DB (219개 테이블) DB_USERNAME=samuser DB_PASSWORD=sampass # sam_stat 연결은 아직 없음 → Phase 1에서 추가 ``` **config/database.php 현재 연결:** - `mysql` - 기본 samdb (원본) - `chandj` - 5130 레거시 DB (사용하지 않음) - `sam_stat` - **아직 없음** (이 작업에서 추가) ### 0.3 기존 대시보드/보고서 시스템 (변경 대상) | 파일 | 경로 | 역할 | 통계 전환 시 영향 | |------|------|------|------------------| | DashboardController | `api/app/Http/Controllers/Api/V1/DashboardController.php` | summary, charts, approvals | Phase 4.5에서 sam_stat 조회로 전환 | | ReportController | `api/app/Http/Controllers/Api/V1/ReportController.php` | daily, expense-estimate, export | Phase 4.5에서 sam_stat 조회로 전환 | | DailyReportController | `api/app/Http/Controllers/Api/V1/DailyReportController.php` | note-receivables, accounts, summary | Phase 4.5에서 sam_stat 조회로 전환 | | DashboardService | `api/app/Services/DashboardService.php` (355줄) | 원본 DB에서 실시간 집계 (Attendance, Approval, Deposit, Sale 등) | **핵심 전환 대상** | | ReportService | `api/app/Services/ReportService.php` | 일일일보, 지출예상 (Excel 내보내기 포함) | 부분 전환 | | DailyReportService | `api/app/Services/DailyReportService.php` | 어음/외상채권, 계좌현황 | 부분 전환 | | AiReportService | `api/app/Services/AiReportService.php` | AI 보고서 생성/조회 | 변경 없음 | **현재 API 라우트 (변경 없음, 내부 데이터소스만 전환):** ``` # common.php GET /api/v1/dashboard/summary → DashboardController@summary GET /api/v1/dashboard/charts → DashboardController@charts GET /api/v1/dashboard/approvals → DashboardController@approvals GET /api/v1/reports/daily → ReportController@daily GET /api/v1/reports/daily/export → ReportController@dailyExport GET /api/v1/reports/expense-estimate → ReportController@expenseEstimate # finance.php GET /api/v1/daily-report/note-receivables → DailyReportController@noteReceivables GET /api/v1/daily-report/daily-accounts → DailyReportController@dailyAccounts GET /api/v1/daily-report/summary → DailyReportController@summary ``` ### 0.4 기존 스케줄러 패턴 (따라야 할 패턴) ```php // api/routes/console.php (Laravel 12 방식 - Kernel.php 없음) use Illuminate\Support\Facades\Schedule; // 기존 스케줄러: 매일 03:00 API 로그 정리 Schedule::command('api-log:prune') ->dailyAt('03:00') ->appendOutputTo(storage_path('logs/scheduler.log')) ->onSuccess(function () { Log::info('...'); }) ->onFailure(function () { Log::error('...'); }); ``` ### 0.5 기존 Artisan 커맨드 패턴 ``` api/app/Console/Commands/ ├── PruneAuditLogs.php # 감사 로그 정리 (참고 패턴) ├── CleanupExpiredLinks.php # 만료 링크 정리 ├── RecordStorageUsage.php # 저장소 사용량 기록 ├── TenantsBootstrap.php # 테넌트 초기화 └── ... # 총 19개 ``` ### 0.6 모델 패턴 (따라야 할 패턴) ```php // 기존 모델 예시 - 멀티테넌트 + Soft Delete namespace App\Models\Tenants; use App\Models\Scopes\TenantScope; use Illuminate\Database\Eloquent\SoftDeletes; class Deposit extends Model { use SoftDeletes; protected $table = 'deposits'; protected static function booted(): void { static::addGlobalScope(new TenantScope); } } // 통계 모델은 다른 DB 연결 사용 // protected $connection = 'sam_stat'; // TenantScope 대신 tenant_id를 직접 WHERE 조건으로 사용 ``` ### 0.7 환경별 구성 #### 로컬 환경 (Docker) ```yaml # docker/docker-compose.yml 내 MySQL 서비스 # Docker 내부 호스트: sam-mysql-1 # sam_stat DB는 같은 MySQL 인스턴스에 생성 (별도 서버 불필요) ``` ```bash # 로컬 sam_stat DB 생성 docker compose exec mysql mysql -u root -proot \ -e "CREATE DATABASE sam_stat CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" # 로컬 마이그레이션 실행 docker compose exec api php artisan migrate --database=sam_stat # 로컬 시딩 docker compose exec api php artisan db:seed --class=DimDateSeeder ``` #### 개발 서버 (non-Docker, codebridge-x.com) > **개발 서버는 Docker를 사용하지 않는다.** > 로컬에서 코드 작업 후 Git push하면 되지만, 개발 서버에서 아래 **1회 세팅이 필요**하다. ```bash # 1. sam_stat DB 생성 (개발 서버 MySQL 직접 접속) mysql -u [user] -p \ -e "CREATE DATABASE sam_stat CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" # 2. .env에 STAT_DB_* 환경변수 추가 (개발 서버의 api/.env) # STAT_DB_HOST=127.0.0.1 # STAT_DB_PORT=3306 # STAT_DB_DATABASE=sam_stat # STAT_DB_USERNAME=[개발서버 DB 유저] # STAT_DB_PASSWORD=[개발서버 DB 비밀번호] # 3. 마이그레이션 실행 cd /path/to/api && php artisan migrate --database=sam_stat # 4. dim_date 시딩 php artisan db:seed --class=DimDateSeeder # 5. 스케줄러 cron 확인 (이미 등록되어 있다면 추가 불필요) # * * * * * cd /path/to/api && php artisan schedule:run >> /dev/null 2>&1 ``` #### 배포 워크플로우 ``` 로컬 (Docker, *.sam.kr) ↓ Git push 개발 서버 (non-Docker, codebridge-x.com) ↓ 수동 배포 ↓ 최초 1회: DB 생성 + .env + migrate + seed + cron 확인 ↓ 이후: git pull → php artisan migrate --database=sam_stat 운영 (TBD) ``` **코드에 커밋되는 것:** `config/database.php`, 마이그레이션, 모델, 서비스, 커맨드 **환경별 수동 설정:** `.env` (STAT_DB_*), DB 생성, cron ### 0.8 핵심 코딩 규칙 (이 작업에 적용) 1. **Service-First**: 비즈니스 로직 → Service, Controller는 DI + 호출만 2. **FormRequest**: Controller에서 직접 검증 금지 3. **BelongsToTenant**: 원본 모델만 적용, 통계 모델은 tenant_id WHERE 직접 사용 4. **i18n**: 메시지는 `__('message.xxx')` 형태 5. **ApiResponse**: `use App\Helpers\ApiResponse;` → `ApiResponse::handle()` 6. **Swagger**: 별도 파일 `api/app/Swagger/v1/{Resource}Api.php`에 작성 7. **커밋**: 사용자 승인 후에만 커밋 (자동 커밋 금지) ### 0.9 작업 시작 체크리스트 ``` 새 세션에서 이 문서를 받았을 때: □ 1. 이 문서의 "📍 현재 진행 상태" 확인 □ 2. Phase별 작업 상태 (⏳/🔄/✅) 확인 □ 3. Docker 실행 확인: docker compose ps (docker/ 디렉토리) □ 4. DB 접속 확인: docker compose exec mysql mysql -u root -proot samdb □ 5. sam_stat DB 존재 여부 확인: SHOW DATABASES LIKE 'sam_stat'; □ 6. 마이그레이션 상태 확인: cd api && php artisan migrate:status □ 7. 다음 작업 항목의 "비고" 컬럼 참조하여 작업 시작 ``` --- ## 1. 개요 ### 1.1 배경 SAM ERP는 219개 테이블, 17개 비즈니스 도메인을 가진 종합 제조/건설 ERP 시스템이다. 현재 대시보드(DashboardService, ReportService 등)는 **원본 DB(samdb)에서 실시간 집계**하는 방식으로 동작한다. **문제점:** - 원본 DB에 집계 쿼리 부하 (JOIN, GROUP BY, SUM 등) - 과거 데이터 추세 분석 불가 (스냅샷 없음) - 도메인별 KPI 누적 관리 불가 - 대시보드 응답 속도 저하 가능성 - 통계 요구사항 증가 시 원본 스키마 오염 **해결 방안:** - `sam_stat` 별도 DB에 사전 집계(pre-aggregated) 통계 데이터 저장 - 배치/스케줄러로 원본(samdb) → 통계(sam_stat) DB 동기화 - 원본 DB 부하 분리, 빠른 조회, 이력 보존 ### 1.2 설계 원칙 ``` ┌─────────────────────────────────────────────────────────────────┐ │ 🎯 핵심 원칙 │ ├─────────────────────────────────────────────────────────────────┤ │ 1. 원본 DB 무간섭 - sam_stat은 읽기 전용 파생 데이터 │ │ 2. 멀티테넌트 유지 - 모든 통계 테이블에 tenant_id 필수 │ │ 3. 시간축 기반 - 일/주/월/분기/년 단위 집계 지원 │ │ 4. 확장 가능 - 새 도메인 통계 추가 시 테이블만 추가 │ │ 5. 멱등성 보장 - 같은 기간 재집계 시 동일 결과 (UPSERT) │ │ 6. 메타데이터 드리븐 - stat_definitions로 동적 통계 정의 가능 │ └─────────────────────────────────────────────────────────────────┘ ``` ### 1.3 변경 승인 정책 | 분류 | 예시 | 승인 | |------|------|------| | ✅ 즉시 가능 | 통계 필드 추가, 집계 주기 변경, 문서 수정 | 불필요 | | ⚠️ 컨펌 필요 | 새 통계 테이블 생성, 스케줄러 추가, 마이그레이션 | **필수** | | 🔴 금지 | 원본 DB 스키마 변경, 원본 테이블에 통계 컬럼 추가 | 별도 협의 | --- ## 2. 분석: 필요한 통계 도메인 SAM의 17개 비즈니스 도메인을 분석하여 8개 핵심 통계 영역을 도출했다. ### 2.1 통계 도메인 매핑 | # | 통계 도메인 | 원본 테이블 | 핵심 지표 | 우선순위 | |---|-----------|-----------|----------|---------| | 1 | **매출/수주** | orders, order_items, sales, clients | 수주액, 매출액, 수주건수, 고객별 매출 | 🔴 P0 | | 2 | **재무/회계** | deposits, withdrawals, purchases, bills, bank_transactions | 입출금, 미수/미지급, 자금흐름, 어음현황 | 🔴 P0 | | 3 | **생산/작업** | work_orders, work_order_items, work_results | 생산량, 작업효율, 불량률, 납기준수율 | 🔴 P0 | | 4 | **재고/자재** | stocks, stock_transactions, material_receipts, shipments | 재고회전율, 입출고량, 안전재고, 로트추적 | 🟡 P1 | | 5 | **견적/영업** | quotes, quote_items, sales_prospects, biddings | 수주전환율, 견적성공률, 영업파이프라인 | 🟡 P1 | | 6 | **인사/근태** | attendance, leaves, payrolls, salaries | 출근율, 근태현황, 인건비, 부서별통계 | 🟡 P1 | | 7 | **건설/프로젝트** | sites, contracts, expected_expenses, labor_distributions | 프로젝트수익률, 공정진행률, 원가분석 | 🟢 P2 | | 8 | **시스템/감사** | audit_logs, api_request_logs, fcm_send_logs | API사용량, 사용자활동, 알림발송률 | 🟢 P2 | --- ## 3. sam_stat 데이터베이스 설계 ### 3.1 아키텍처 개요 ``` ┌──────────────────────────────────────────────────────────────────┐ │ sam_stat DB │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────┐ ┌─────────────────────────────────┐ │ │ │ 메타 테이블 (2) │ │ 이벤트/팩트 테이블 (2) │ │ │ │ │ │ │ │ │ │ stat_definitions │ │ stat_events │ │ │ │ stat_job_logs │ │ stat_snapshots │ │ │ └─────────────────────┘ └─────────────────────────────────┘ │ │ │ │ ┌───────────────────────────────────────────────────────────┐ │ │ │ 도메인별 집계 테이블 (8 도메인) │ │ │ │ │ │ │ │ stat_sales_daily stat_inventory_daily │ │ │ │ stat_finance_daily stat_quote_pipeline_daily │ │ │ │ stat_production_daily stat_hr_attendance_daily │ │ │ │ stat_project_monthly stat_system_daily │ │ │ │ │ │ │ │ 요약 테이블 (월간/연간) │ │ │ │ │ │ │ │ stat_sales_monthly stat_finance_monthly │ │ │ │ stat_production_monthly stat_kpi_monthly │ │ │ │ │ │ │ └───────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────┐ ┌─────────────────────────────────┐ │ │ │ 차원 테이블 (Dim) │ │ KPI/알림 테이블 │ │ │ │ │ │ │ │ │ │ dim_date │ │ stat_kpi_targets │ │ │ │ dim_client │ │ stat_alerts │ │ │ │ dim_product │ │ │ │ │ └─────────────────────┘ └─────────────────────────────────┘ │ │ │ │ 총 테이블: 18개 │ └──────────────────────────────────────────────────────────────────┘ ``` ### 3.2 데이터 흐름 ``` samdb (원본) sam_stat (통계) ┌──────────┐ ┌──────────────┐ │ orders │──┐ │ │ │ sales │──┤ Scheduler │ stat_sales_ │ │ deposits │──┼──(매일 02:00)──→│ daily │ │ stocks │──┤ │ │ │ work_ │──┤ │ stat_finance_│ │ orders │──┘ │ daily │ │ │ │ │ │ │ Scheduler │ stat_*_ │ │ │──(매월 1일)──────→│ monthly │ │ │ │ │ │ │ 실시간 이벤트 │ stat_events │ │ │──(Observer)─────→│ │ └──────────┘ └──────────────┘ ``` --- ## 4. 테이블 상세 설계 ### 4.1 메타 테이블 #### `stat_definitions` - 통계 정의 (메타데이터 드리븐) ```sql CREATE TABLE stat_definitions ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, code VARCHAR(100) NOT NULL UNIQUE, -- 'sales_daily_revenue' domain VARCHAR(50) NOT NULL, -- 'sales', 'finance', 'production' name VARCHAR(200) NOT NULL, -- '일일 매출액' description TEXT NULL, source_tables JSON NOT NULL, -- ["orders", "order_items", "sales"] aggregation VARCHAR(20) NOT NULL DEFAULT 'daily', -- daily, weekly, monthly, quarterly, yearly query_template TEXT NULL, -- 집계 SQL 템플릿 (선택) is_active BOOLEAN NOT NULL DEFAULT TRUE, config JSON NULL, -- 추가 설정 (임계값, 단위 등) created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL, INDEX idx_domain (domain), INDEX idx_aggregation (aggregation), INDEX idx_active (is_active) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` #### `stat_job_logs` - 집계 작업 이력 ```sql CREATE TABLE stat_job_logs ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, job_type VARCHAR(100) NOT NULL, -- 'sales_daily', 'finance_monthly' target_date DATE NOT NULL, -- 집계 대상 날짜 status ENUM('pending','running','completed','failed') NOT NULL DEFAULT 'pending', records_processed INT UNSIGNED DEFAULT 0, error_message TEXT NULL, started_at TIMESTAMP NULL, completed_at TIMESTAMP NULL, duration_ms INT UNSIGNED NULL, created_at TIMESTAMP NULL, INDEX idx_tenant_job (tenant_id, job_type), INDEX idx_status (status), INDEX idx_target_date (target_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` --- ### 4.2 차원 테이블 (Dimension) #### `dim_date` - 날짜 차원 ```sql CREATE TABLE dim_date ( date_key DATE PRIMARY KEY, -- '2026-01-29' year SMALLINT NOT NULL, quarter TINYINT NOT NULL, -- 1~4 month TINYINT NOT NULL, week TINYINT NOT NULL, -- ISO week day_of_week TINYINT NOT NULL, -- 1(월)~7(일) day_of_month TINYINT NOT NULL, is_weekend BOOLEAN NOT NULL, is_holiday BOOLEAN NOT NULL DEFAULT FALSE, holiday_name VARCHAR(100) NULL, fiscal_year SMALLINT NULL, -- 회계연도 fiscal_quarter TINYINT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` #### `dim_client` - 고객 차원 (스냅샷) ```sql CREATE TABLE dim_client ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, client_id BIGINT UNSIGNED NOT NULL, -- 원본 clients.id client_name VARCHAR(200) NOT NULL, client_group_id BIGINT UNSIGNED NULL, client_group_name VARCHAR(200) NULL, client_type VARCHAR(50) NULL, -- 고객/공급업체/양쪽 region VARCHAR(100) NULL, valid_from DATE NOT NULL, valid_to DATE NULL, -- NULL = 현재 유효 is_current BOOLEAN NOT NULL DEFAULT TRUE, INDEX idx_tenant_client (tenant_id, client_id), INDEX idx_current (is_current) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` #### `dim_product` - 제품 차원 (스냅샷) ```sql CREATE TABLE dim_product ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, product_id BIGINT UNSIGNED NOT NULL, -- 원본 products.id product_code VARCHAR(100) NOT NULL, product_name VARCHAR(300) NOT NULL, product_type VARCHAR(50) NULL, -- PRODUCT/PART/SUBASSEMBLY category_id BIGINT UNSIGNED NULL, category_name VARCHAR(200) NULL, valid_from DATE NOT NULL, valid_to DATE NULL, is_current BOOLEAN NOT NULL DEFAULT TRUE, INDEX idx_tenant_product (tenant_id, product_id), INDEX idx_current (is_current) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` --- ### 4.3 도메인별 집계 테이블 (Fact) #### 🔴 P0: `stat_sales_daily` - 매출/수주 일일 통계 ```sql CREATE TABLE stat_sales_daily ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, stat_date DATE NOT NULL, -- 수주 order_count INT UNSIGNED DEFAULT 0, -- 신규 수주 건수 order_amount DECIMAL(18,2) DEFAULT 0, -- 수주 금액 order_item_count INT UNSIGNED DEFAULT 0, -- 수주 품목 수 -- 매출 sales_count INT UNSIGNED DEFAULT 0, -- 매출 건수 sales_amount DECIMAL(18,2) DEFAULT 0, -- 매출 금액 sales_tax_amount DECIMAL(18,2) DEFAULT 0, -- 세액 -- 고객 new_client_count INT UNSIGNED DEFAULT 0, -- 신규 고객 수 active_client_count INT UNSIGNED DEFAULT 0, -- 활성 고객 수 -- 수주 상태별 건수 order_draft_count INT UNSIGNED DEFAULT 0, order_confirmed_count INT UNSIGNED DEFAULT 0, order_in_progress_count INT UNSIGNED DEFAULT 0, order_completed_count INT UNSIGNED DEFAULT 0, order_cancelled_count INT UNSIGNED DEFAULT 0, -- 출하 shipment_count INT UNSIGNED DEFAULT 0, shipment_amount DECIMAL(18,2) DEFAULT 0, created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL, UNIQUE KEY uk_tenant_date (tenant_id, stat_date), INDEX idx_date (stat_date), INDEX idx_tenant (tenant_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` #### 🔴 P0: `stat_finance_daily` - 재무 일일 통계 ```sql CREATE TABLE stat_finance_daily ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, stat_date DATE NOT NULL, -- 입출금 deposit_count INT UNSIGNED DEFAULT 0, deposit_amount DECIMAL(18,2) DEFAULT 0, withdrawal_count INT UNSIGNED DEFAULT 0, withdrawal_amount DECIMAL(18,2) DEFAULT 0, net_cashflow DECIMAL(18,2) DEFAULT 0, -- 입금 - 출금 -- 매입 purchase_count INT UNSIGNED DEFAULT 0, purchase_amount DECIMAL(18,2) DEFAULT 0, purchase_tax_amount DECIMAL(18,2) DEFAULT 0, -- 미수/미지급 receivable_balance DECIMAL(18,2) DEFAULT 0, -- 미수금 잔액 payable_balance DECIMAL(18,2) DEFAULT 0, -- 미지급 잔액 overdue_receivable DECIMAL(18,2) DEFAULT 0, -- 연체 미수금 -- 어음 bill_issued_count INT UNSIGNED DEFAULT 0, bill_issued_amount DECIMAL(18,2) DEFAULT 0, bill_matured_count INT UNSIGNED DEFAULT 0, bill_matured_amount DECIMAL(18,2) DEFAULT 0, -- 카드 card_transaction_count INT UNSIGNED DEFAULT 0, card_transaction_amount DECIMAL(18,2) DEFAULT 0, -- 은행 bank_balance_total DECIMAL(18,2) DEFAULT 0, -- 전 계좌 잔액 합계 created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL, UNIQUE KEY uk_tenant_date (tenant_id, stat_date), INDEX idx_date (stat_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` #### 🔴 P0: `stat_production_daily` - 생산 일일 통계 ```sql CREATE TABLE stat_production_daily ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, stat_date DATE NOT NULL, -- 작업지시 wo_created_count INT UNSIGNED DEFAULT 0, -- 신규 작업지시 wo_completed_count INT UNSIGNED DEFAULT 0, -- 완료 작업지시 wo_in_progress_count INT UNSIGNED DEFAULT 0, -- 진행중 wo_overdue_count INT UNSIGNED DEFAULT 0, -- 납기 초과 -- 생산량 production_qty DECIMAL(18,2) DEFAULT 0, -- 생산 수량 defect_qty DECIMAL(18,2) DEFAULT 0, -- 불량 수량 defect_rate DECIMAL(5,2) DEFAULT 0, -- 불량률 (%) -- 작업 효율 planned_hours DECIMAL(10,2) DEFAULT 0, -- 계획 공수 actual_hours DECIMAL(10,2) DEFAULT 0, -- 실적 공수 efficiency_rate DECIMAL(5,2) DEFAULT 0, -- 효율 (%) -- 작업자 active_worker_count INT UNSIGNED DEFAULT 0, issue_count INT UNSIGNED DEFAULT 0, -- 발생 이슈 수 -- 납기 on_time_delivery_count INT UNSIGNED DEFAULT 0, late_delivery_count INT UNSIGNED DEFAULT 0, delivery_rate DECIMAL(5,2) DEFAULT 0, -- 납기준수율 (%) created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL, UNIQUE KEY uk_tenant_date (tenant_id, stat_date), INDEX idx_date (stat_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` #### 🟡 P1: `stat_inventory_daily` - 재고 일일 통계 ```sql CREATE TABLE stat_inventory_daily ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, stat_date DATE NOT NULL, -- 재고 현황 total_sku_count INT UNSIGNED DEFAULT 0, -- 총 SKU 수 total_stock_qty DECIMAL(18,2) DEFAULT 0, -- 총 재고 수량 total_stock_value DECIMAL(18,2) DEFAULT 0, -- 총 재고 금액 -- 입출고 receipt_count INT UNSIGNED DEFAULT 0, -- 입고 건수 receipt_qty DECIMAL(18,2) DEFAULT 0, receipt_amount DECIMAL(18,2) DEFAULT 0, issue_count INT UNSIGNED DEFAULT 0, -- 출고 건수 issue_qty DECIMAL(18,2) DEFAULT 0, issue_amount DECIMAL(18,2) DEFAULT 0, -- 안전재고 below_safety_count INT UNSIGNED DEFAULT 0, -- 안전재고 미달 품목 수 zero_stock_count INT UNSIGNED DEFAULT 0, -- 재고 0 품목 수 excess_stock_count INT UNSIGNED DEFAULT 0, -- 과잉 재고 품목 수 -- 품질검사 inspection_count INT UNSIGNED DEFAULT 0, inspection_pass_count INT UNSIGNED DEFAULT 0, inspection_fail_count INT UNSIGNED DEFAULT 0, inspection_pass_rate DECIMAL(5,2) DEFAULT 0, -- 합격률 (%) -- 재고회전 turnover_rate DECIMAL(8,2) DEFAULT 0, -- 재고회전율 created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL, UNIQUE KEY uk_tenant_date (tenant_id, stat_date), INDEX idx_date (stat_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` #### 🟡 P1: `stat_quote_pipeline_daily` - 견적/영업 일일 통계 ```sql CREATE TABLE stat_quote_pipeline_daily ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, stat_date DATE NOT NULL, -- 견적 quote_created_count INT UNSIGNED DEFAULT 0, quote_amount DECIMAL(18,2) DEFAULT 0, quote_approved_count INT UNSIGNED DEFAULT 0, quote_rejected_count INT UNSIGNED DEFAULT 0, quote_conversion_count INT UNSIGNED DEFAULT 0, -- 수주 전환 건수 quote_conversion_rate DECIMAL(5,2) DEFAULT 0, -- 전환율 (%) -- 영업 기회 prospect_created_count INT UNSIGNED DEFAULT 0, prospect_won_count INT UNSIGNED DEFAULT 0, prospect_lost_count INT UNSIGNED DEFAULT 0, prospect_amount DECIMAL(18,2) DEFAULT 0, -- 파이프라인 금액 -- 입찰 bidding_count INT UNSIGNED DEFAULT 0, bidding_won_count INT UNSIGNED DEFAULT 0, bidding_amount DECIMAL(18,2) DEFAULT 0, -- 상담 consultation_count INT UNSIGNED DEFAULT 0, created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL, UNIQUE KEY uk_tenant_date (tenant_id, stat_date), INDEX idx_date (stat_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` #### 🟡 P1: `stat_hr_attendance_daily` - 인사/근태 일일 통계 ```sql CREATE TABLE stat_hr_attendance_daily ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, stat_date DATE NOT NULL, -- 근태 total_employees INT UNSIGNED DEFAULT 0, -- 전체 직원 수 attendance_count INT UNSIGNED DEFAULT 0, -- 출근 인원 late_count INT UNSIGNED DEFAULT 0, -- 지각 absent_count INT UNSIGNED DEFAULT 0, -- 결근 attendance_rate DECIMAL(5,2) DEFAULT 0, -- 출근율 (%) -- 휴가 leave_count INT UNSIGNED DEFAULT 0, -- 휴가 사용 leave_annual_count INT UNSIGNED DEFAULT 0, -- 연차 leave_sick_count INT UNSIGNED DEFAULT 0, -- 병가 leave_other_count INT UNSIGNED DEFAULT 0, -- 기타 -- 초과근무 overtime_hours DECIMAL(10,2) DEFAULT 0, overtime_employee_count INT UNSIGNED DEFAULT 0, -- 인건비 (급여 정산 기준) total_labor_cost DECIMAL(18,2) DEFAULT 0, created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL, UNIQUE KEY uk_tenant_date (tenant_id, stat_date), INDEX idx_date (stat_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` #### 🟢 P2: `stat_project_monthly` - 건설/프로젝트 월간 통계 ```sql CREATE TABLE stat_project_monthly ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, stat_year SMALLINT NOT NULL, stat_month TINYINT NOT NULL, -- 프로젝트 현황 active_site_count INT UNSIGNED DEFAULT 0, completed_site_count INT UNSIGNED DEFAULT 0, new_contract_count INT UNSIGNED DEFAULT 0, contract_total_amount DECIMAL(18,2) DEFAULT 0, -- 원가 expected_expense_total DECIMAL(18,2) DEFAULT 0, actual_expense_total DECIMAL(18,2) DEFAULT 0, labor_cost_total DECIMAL(18,2) DEFAULT 0, material_cost_total DECIMAL(18,2) DEFAULT 0, -- 수익률 gross_profit DECIMAL(18,2) DEFAULT 0, gross_profit_rate DECIMAL(5,2) DEFAULT 0, -- 수익률 (%) -- 이슈 handover_report_count INT UNSIGNED DEFAULT 0, structure_review_count INT UNSIGNED DEFAULT 0, created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL, UNIQUE KEY uk_tenant_year_month (tenant_id, stat_year, stat_month), INDEX idx_year_month (stat_year, stat_month) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` #### 🟢 P2: `stat_system_daily` - 시스템 일일 통계 ```sql CREATE TABLE stat_system_daily ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, stat_date DATE NOT NULL, -- API 사용량 api_request_count INT UNSIGNED DEFAULT 0, api_error_count INT UNSIGNED DEFAULT 0, api_avg_response_ms INT UNSIGNED DEFAULT 0, -- 사용자 활동 active_user_count INT UNSIGNED DEFAULT 0, login_count INT UNSIGNED DEFAULT 0, -- 감사 audit_create_count INT UNSIGNED DEFAULT 0, audit_update_count INT UNSIGNED DEFAULT 0, audit_delete_count INT UNSIGNED DEFAULT 0, -- 알림 fcm_sent_count INT UNSIGNED DEFAULT 0, fcm_failed_count INT UNSIGNED DEFAULT 0, -- 파일 file_upload_count INT UNSIGNED DEFAULT 0, file_upload_size_mb DECIMAL(10,2) DEFAULT 0, -- 결재 approval_submitted_count INT UNSIGNED DEFAULT 0, approval_completed_count INT UNSIGNED DEFAULT 0, approval_avg_hours DECIMAL(8,2) DEFAULT 0, -- 평균 처리 시간 created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL, UNIQUE KEY uk_tenant_date (tenant_id, stat_date), INDEX idx_date (stat_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` --- ### 4.4 월간 요약 테이블 #### `stat_sales_monthly` - 매출 월간 요약 ```sql CREATE TABLE stat_sales_monthly ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, stat_year SMALLINT NOT NULL, stat_month TINYINT NOT NULL, -- 일일 합산 order_count INT UNSIGNED DEFAULT 0, order_amount DECIMAL(18,2) DEFAULT 0, sales_count INT UNSIGNED DEFAULT 0, sales_amount DECIMAL(18,2) DEFAULT 0, shipment_count INT UNSIGNED DEFAULT 0, shipment_amount DECIMAL(18,2) DEFAULT 0, -- 월간 고유 지표 unique_client_count INT UNSIGNED DEFAULT 0, -- 거래 고객 수 avg_order_amount DECIMAL(18,2) DEFAULT 0, -- 평균 수주 금액 top_client_id BIGINT UNSIGNED NULL, -- 최다 거래 고객 top_client_amount DECIMAL(18,2) DEFAULT 0, mom_growth_rate DECIMAL(8,2) NULL, -- 전월 대비 성장률 (%) yoy_growth_rate DECIMAL(8,2) NULL, -- 전년동월 대비 (%) created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL, UNIQUE KEY uk_tenant_year_month (tenant_id, stat_year, stat_month), INDEX idx_year_month (stat_year, stat_month) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` #### `stat_finance_monthly` - 재무 월간 요약 ```sql CREATE TABLE stat_finance_monthly ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, stat_year SMALLINT NOT NULL, stat_month TINYINT NOT NULL, deposit_total DECIMAL(18,2) DEFAULT 0, withdrawal_total DECIMAL(18,2) DEFAULT 0, net_cashflow DECIMAL(18,2) DEFAULT 0, purchase_total DECIMAL(18,2) DEFAULT 0, card_total DECIMAL(18,2) DEFAULT 0, receivable_end DECIMAL(18,2) DEFAULT 0, -- 월말 미수금 payable_end DECIMAL(18,2) DEFAULT 0, -- 월말 미지급 bank_balance_end DECIMAL(18,2) DEFAULT 0, -- 월말 잔액 mom_cashflow_change DECIMAL(8,2) NULL, -- 전월 대비 현금흐름 변화 (%) created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL, UNIQUE KEY uk_tenant_year_month (tenant_id, stat_year, stat_month), INDEX idx_year_month (stat_year, stat_month) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` #### `stat_production_monthly` - 생산 월간 요약 ```sql CREATE TABLE stat_production_monthly ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, stat_year SMALLINT NOT NULL, stat_month TINYINT NOT NULL, wo_total_count INT UNSIGNED DEFAULT 0, wo_completed_count INT UNSIGNED DEFAULT 0, production_qty DECIMAL(18,2) DEFAULT 0, defect_qty DECIMAL(18,2) DEFAULT 0, avg_defect_rate DECIMAL(5,2) DEFAULT 0, avg_efficiency_rate DECIMAL(5,2) DEFAULT 0, avg_delivery_rate DECIMAL(5,2) DEFAULT 0, total_planned_hours DECIMAL(10,2) DEFAULT 0, total_actual_hours DECIMAL(10,2) DEFAULT 0, issue_total_count INT UNSIGNED DEFAULT 0, created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL, UNIQUE KEY uk_tenant_year_month (tenant_id, stat_year, stat_month), INDEX idx_year_month (stat_year, stat_month) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` --- ### 4.5 KPI/알림 테이블 #### `stat_kpi_targets` - KPI 목표 설정 ```sql CREATE TABLE stat_kpi_targets ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, stat_year SMALLINT NOT NULL, stat_month TINYINT NULL, -- NULL = 연간 목표 domain VARCHAR(50) NOT NULL, -- 'sales', 'production' metric_code VARCHAR(100) NOT NULL, -- 'monthly_sales_amount' target_value DECIMAL(18,2) NOT NULL, unit VARCHAR(20) NOT NULL DEFAULT 'KRW', -- KRW, %, count, hours description VARCHAR(300) NULL, created_by BIGINT UNSIGNED NULL, created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL, UNIQUE KEY uk_tenant_metric (tenant_id, stat_year, stat_month, metric_code), INDEX idx_domain (domain) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` #### `stat_alerts` - 통계 기반 알림 ```sql CREATE TABLE stat_alerts ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, domain VARCHAR(50) NOT NULL, alert_type VARCHAR(100) NOT NULL, -- 'below_target', 'anomaly', 'threshold' severity ENUM('info','warning','critical') NOT NULL DEFAULT 'info', title VARCHAR(300) NOT NULL, message TEXT NOT NULL, metric_code VARCHAR(100) NULL, current_value DECIMAL(18,2) NULL, threshold_value DECIMAL(18,2) NULL, is_read BOOLEAN NOT NULL DEFAULT FALSE, is_resolved BOOLEAN NOT NULL DEFAULT FALSE, resolved_at TIMESTAMP NULL, resolved_by BIGINT UNSIGNED NULL, created_at TIMESTAMP NULL, INDEX idx_tenant_unread (tenant_id, is_read), INDEX idx_severity (severity), INDEX idx_domain (domain) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` --- ### 4.6 이벤트/스냅샷 테이블 #### `stat_events` - 실시간 이벤트 로그 (확장용) ```sql CREATE TABLE stat_events ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, domain VARCHAR(50) NOT NULL, event_type VARCHAR(100) NOT NULL, -- 'order_created', 'payment_received' entity_type VARCHAR(100) NOT NULL, -- 'Order', 'Deposit' entity_id BIGINT UNSIGNED NOT NULL, payload JSON NULL, -- 이벤트 데이터 occurred_at TIMESTAMP NOT NULL, INDEX idx_tenant_domain (tenant_id, domain), INDEX idx_occurred (occurred_at), INDEX idx_entity (entity_type, entity_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` #### `stat_snapshots` - 상태 스냅샷 (특정 시점 전체 상태) ```sql CREATE TABLE stat_snapshots ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, tenant_id BIGINT UNSIGNED NOT NULL, snapshot_date DATE NOT NULL, domain VARCHAR(50) NOT NULL, snapshot_type VARCHAR(50) NOT NULL DEFAULT 'daily', -- daily, weekly, monthly data JSON NOT NULL, -- 전체 스냅샷 데이터 created_at TIMESTAMP NULL, UNIQUE KEY uk_tenant_date_domain (tenant_id, snapshot_date, domain, snapshot_type), INDEX idx_date (snapshot_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` --- ## 5. 테이블 요약 | # | 테이블명 | 유형 | 도메인 | 집계 주기 | 우선순위 | |---|---------|------|--------|----------|---------| | 1 | `stat_definitions` | 메타 | 공통 | - | 🔴 P0 | | 2 | `stat_job_logs` | 메타 | 공통 | - | 🔴 P0 | | 3 | `dim_date` | 차원 | 공통 | 1회 생성 | 🔴 P0 | | 4 | `dim_client` | 차원 | 공통 | SCD Type 2 | 🟡 P1 | | 5 | `dim_product` | 차원 | 공통 | SCD Type 2 | 🟡 P1 | | 6 | `stat_sales_daily` | 팩트 | 매출/수주 | 일간 | 🔴 P0 | | 7 | `stat_finance_daily` | 팩트 | 재무/회계 | 일간 | 🔴 P0 | | 8 | `stat_production_daily` | 팩트 | 생산/작업 | 일간 | 🔴 P0 | | 9 | `stat_inventory_daily` | 팩트 | 재고/자재 | 일간 | 🟡 P1 | | 10 | `stat_quote_pipeline_daily` | 팩트 | 견적/영업 | 일간 | 🟡 P1 | | 11 | `stat_hr_attendance_daily` | 팩트 | 인사/근태 | 일간 | 🟡 P1 | | 12 | `stat_project_monthly` | 팩트 | 건설/프로젝트 | 월간 | 🟢 P2 | | 13 | `stat_system_daily` | 팩트 | 시스템/감사 | 일간 | 🟢 P2 | | 14 | `stat_sales_monthly` | 요약 | 매출/수주 | 월간 | 🔴 P0 | | 15 | `stat_finance_monthly` | 요약 | 재무/회계 | 월간 | 🔴 P0 | | 16 | `stat_production_monthly` | 요약 | 생산/작업 | 월간 | 🔴 P0 | | 17 | `stat_kpi_targets` | KPI | 공통 | 수동 설정 | 🟡 P1 | | 18 | `stat_alerts` | 알림 | 공통 | 실시간 | 🟡 P1 | | 19 | `stat_events` | 이벤트 | 공통 | 실시간 | 🟢 P2 | | 20 | `stat_snapshots` | 스냅샷 | 공통 | 일/월 | 🟢 P2 | **총 20개 테이블** (메타 2 + 차원 3 + 일간팩트 6 + 월간팩트 1 + 월간요약 3 + KPI/알림 2 + 이벤트/스냅샷 2 + 시스템 1) --- ## 6. 구현 계획 (Phase) ### Phase 1: 인프라 구축 (P0) | # | 작업 항목 | 상태 | 구체적 작업 내용 | |---|----------|:----:|-----------------| | 1.1 | sam_stat DB 생성 및 Laravel 연결 설정 | ✅ | ① Docker MySQL에 `CREATE DATABASE sam_stat` 실행 ② `api/config/database.php`에 `sam_stat` 연결 추가 ③ `api/.env`에 `STAT_DB_*` 환경변수 추가 | | 1.2 | 메타 테이블 마이그레이션 | ✅ | `stat_definitions`, `stat_job_logs` 마이그레이션 생성 (`--database=sam_stat` 옵션) | | 1.3 | dim_date 테이블 생성 및 시딩 | ✅ | 2020-01-01~2030-12-31 날짜 데이터 Seeder 작성 (4,018건) | | 1.4 | 기반 모델 클래스 생성 | ✅ | `BaseStatModel`, `StatDefinition`, `StatJobLog`, `DimDate` 생성 | | 1.5 | 집계 커맨드 기반 구조 | ✅ | `StatAggregateDailyCommand.php`, `StatAggregateMonthlyCommand.php` 생성 | | 1.6 | StatAggregatorService 골격 | ✅ | `StatAggregatorService.php` + `StatDomainServiceInterface.php` - 테넌트 순회 + 도메인별 서비스 호출 구조 | **Phase 1 검증 방법:** ```bash # DB 생성 확인 docker compose exec mysql mysql -u root -proot -e "SHOW DATABASES LIKE 'sam_stat';" # 마이그레이션 실행 cd api && php artisan migrate --database=sam_stat # dim_date 시딩 cd api && php artisan db:seed --class=DimDateSeeder # 커맨드 확인 cd api && php artisan stat:aggregate-daily --help ``` ### Phase 2: P0 도메인 구축 | # | 작업 항목 | 상태 | 구체적 작업 내용 | |---|----------|:----:|-----------------| | 2.1 | 매출 테이블 마이그레이션 | ✅ | `stat_sales_daily` + `stat_sales_monthly` 마이그레이션 | | 2.2 | 매출 모델 + 서비스 | ✅ | `StatSalesDaily`, `StatSalesMonthly`, `SalesStatService` - orders, sales, clients, shipments 집계 | | 2.3 | 재무 테이블 마이그레이션 | ✅ | `stat_finance_daily` + `stat_finance_monthly` 마이그레이션 | | 2.4 | 재무 모델 + 서비스 | ✅ | `StatFinanceDaily`, `StatFinanceMonthly`, `FinanceStatService` - deposits, withdrawals, purchases, bills, bank_transactions 집계 | | 2.5 | 생산 테이블 마이그레이션 | ✅ | `stat_production_daily` + `stat_production_monthly` 마이그레이션 | | 2.6 | 생산 모델 + 서비스 | ✅ | `StatProductionDaily`, `StatProductionMonthly`, `ProductionStatService` - work_orders, work_results 집계 | | 2.7 | 스케줄러 등록 | ✅ | `console.php`에 `stat:aggregate-daily` (02:00), `stat:aggregate-monthly` (매월 1일 03:00) 등록 | **Phase 2 검증 방법:** ```bash # 수동 집계 실행 (특정 날짜) cd api && php artisan stat:aggregate-daily --date=2026-01-28 # 데이터 확인 docker compose exec mysql mysql -u root -proot sam_stat \ -e "SELECT * FROM stat_sales_daily WHERE stat_date='2026-01-28';" ``` ### Phase 3: P1 도메인 확장 | # | 작업 항목 | 상태 | 구체적 작업 내용 | |---|----------|:----:|-----------------| | 3.1 | 차원 테이블 | ✅ | `dim_client`, `dim_product` 마이그레이션 + 모델 + `DimensionSyncService` (SCD Type 2). 원본: `clients`→`dim_client`, `items`→`dim_product` (products 테이블 없어 items 사용) | | 3.2 | 재고 통계 | ✅ | `stat_inventory_daily` 마이그레이션 + 모델 + `InventoryStatService` - 원본: `stocks`, `stock_transactions`, `inspections` | | 3.3 | 견적/영업 통계 | ✅ | `stat_quote_pipeline_daily` 마이그레이션 + 모델 + `QuoteStatService` - 원본: `quotes`, `sales_prospects`, `biddings`, `sales_prospect_consultations` | | 3.4 | 인사/근태 통계 | ✅ | `stat_hr_attendance_daily` 마이그레이션 + 모델 + `HrStatService` - 원본: `attendances`, `leaves`, `user_tenants` | | 3.5 | KPI/알림 | ✅ | `stat_kpi_targets`, `stat_alerts` 마이그레이션 + 모델 + `KpiAlertService` + `StatCheckKpiAlertsCommand` + 스케줄러 09:00 | ### Phase 4: P2 도메인 + API + 대시보드 전환 | # | 작업 항목 | 상태 | 구체적 작업 내용 | |---|----------|:----:|-----------------| | 4.1 | 건설/프로젝트 통계 | ✅ | `stat_project_monthly` 마이그레이션 + 모델 + `ProjectStatService` - 원본: `sites`, `contracts`, `expected_expenses`. 월간 전용 도메인 | | 4.2 | 시스템 통계 | ✅ | `stat_system_daily` 마이그레이션 + 모델 + `SystemStatService` - 원본: `api_request_logs`, `personal_access_tokens`(user_tenants 조인), `audit_logs`, `fcm_send_logs`, `files`, `approvals` | | 4.3 | 이벤트/스냅샷 | ✅ | `stat_events`, `stat_snapshots` 마이그레이션 + 모델 + `StatEventService` + `StatEventObserver` (Order, Sale, Deposit, Withdrawal, Purchase, Approval에 등록) | | 4.4 | 통계 API | ✅ | `StatController` (summary/daily/monthly/alerts) + `StatQueryService` + FormRequest 3개 + `routes/api/v1/stats.php`. Swagger는 Phase 5에서 추가 | | 4.5 | 대시보드 전환 | ✅ | `DashboardService` getFinanceSummary/getSalesSummary → sam_stat 우선 조회 + 원본 DB 폴백. 응답에 `source` 필드 추가 | ### Phase 5: 최적화 및 안정화 | # | 작업 항목 | 상태 | 구체적 작업 내용 | |---|----------|:----:|-----------------| | 5.1 | 백필 스크립트 | ✅ | `StatBackfillCommand` - `stat:backfill --from= --to= --domain= --tenant= --skip-monthly --skip-dimensions`. CarbonPeriod 일간 순회 + 월간 집계 + 프로그레스바 + 에러 리포트. 테스트: 7도메인 0.2초 | | 5.2 | 정합성 검증 | ✅ | `StatVerifyCommand` - `stat:verify --date= --tenant= --domain= --fix`. sales(수주건수/매출금액), finance(입금액/출금액), system(API요청수/감사로그수) 교차 검증. --fix 시 자동 재집계. 테스트: 6건 전부 일치 | | 5.3 | 파티셔닝 준비 | ✅ | `2026_01_29_300001_prepare_partitioning_daily_tables.php` - 7개 일간 테이블 RANGE COLUMNS(stat_date) 파티셔닝. PK에 stat_date 포함, p2024~p2028 + p_future. 기존 파티션 여부 체크 후 스킵 | | 5.4 | Redis 캐싱 | ✅ | `StatQueryService` - Cache::remember TTL 5분. 키 패턴: `stat:{daily\|monthly\|dashboard}:{tenantId}:...`. `invalidateCache()` 정적 메서드: Redis keys 패턴 매칭 삭제. 집계 완료 시 StatAggregatorService에서 자동 호출 | | 5.5 | 모니터링 알림 | ✅ | `StatMonitorService` - recordAggregationFailure(critical), recordMissingData(warning), recordMismatch(critical), resolveAlerts(). StatAggregatorService catch 블록에서 자동 호출. stat_alerts 테이블 연동 검증 완료 | ### Phase 6: 문서화 및 마무리 | # | 작업 항목 | 상태 | 구체적 작업 내용 | |---|----------|:----:|-----------------| | 6.1 | Swagger API 문서 | ✅ | `app/Swagger/v1/StatApi.php` - Stats 태그, 4개 엔드포인트 (summary/daily/monthly/alerts), StatSalesDaily/StatFinanceDaily/StatDashboardSummary/StatAlert 스키마 정의. `l5-swagger:generate` 성공 | | 6.2 | DB 스키마 문서 | ✅ | `docs/specs/database-schema.md`에 sam_stat 섹션 추가 - 20개 테이블 (메타 2, 차원 3, 일간 7, 월간 4, KPI/알림/이벤트 4) + Artisan 커맨드 5개 + API 엔드포인트 4개 | | 6.3 | 계획 문서 완료 | ✅ | Phase 6 섹션 추가, 진행률 100%, 상태 완료 | --- ## 7. 기술 설계 요약 ### 7.1 Laravel 다중 DB 연결 ```php // config/database.php 'connections' => [ 'mysql' => [ /* 기존 samdb */ ], 'sam_stat' => [ 'driver' => 'mysql', 'host' => env('STAT_DB_HOST', '127.0.0.1'), 'database' => env('STAT_DB_DATABASE', 'sam_stat'), 'username' => env('STAT_DB_USERNAME', 'root'), 'password' => env('STAT_DB_PASSWORD', ''), // ... 나머지 동일 ], ], ``` ### 7.2 모델 구조 ``` api/app/Models/Stats/ ├── StatDefinition.php // connection = 'sam_stat' ├── StatJobLog.php ├── Dimensions/ │ ├── DimDate.php │ ├── DimClient.php │ └── DimProduct.php ├── Daily/ │ ├── StatSalesDaily.php │ ├── StatFinanceDaily.php │ ├── StatProductionDaily.php │ ├── StatInventoryDaily.php │ ├── StatQuotePipelineDaily.php │ ├── StatHrAttendanceDaily.php │ └── StatSystemDaily.php ├── Monthly/ │ ├── StatSalesMonthly.php │ ├── StatFinanceMonthly.php │ ├── StatProductionMonthly.php │ └── StatProjectMonthly.php ├── StatKpiTarget.php ├── StatAlert.php ├── StatEvent.php └── StatSnapshot.php ``` ### 7.3 서비스 구조 ``` api/app/Services/Stats/ ├── StatAggregatorService.php // 집계 오케스트레이터 ├── SalesStatService.php // 매출/수주 집계 ├── FinanceStatService.php // 재무 집계 ├── ProductionStatService.php // 생산 집계 ├── InventoryStatService.php // 재고 집계 ├── QuoteStatService.php // 견적/영업 집계 ├── HrStatService.php // 인사/근태 집계 ├── ProjectStatService.php // 건설 집계 ├── SystemStatService.php // 시스템 집계 └── KpiAlertService.php // KPI 목표 대비 알림 ``` ### 7.4 스케줄러 구조 ```php // app/Console/Kernel.php (또는 routes/console.php) // 일간 집계 - 매일 02:00 Schedule::command('stat:aggregate-daily') ->dailyAt('02:00') ->withoutOverlapping(); // 월간 집계 - 매월 1일 03:00 Schedule::command('stat:aggregate-monthly') ->monthlyOn(1, '03:00') ->withoutOverlapping(); // KPI 알림 체크 - 매일 09:00 Schedule::command('stat:check-kpi-alerts') ->dailyAt('09:00'); ``` ### 7.5 집계 패턴 (UPSERT) ```php // 멱등성 보장: 같은 날짜 재실행 시 덮어쓰기 StatSalesDaily::updateOrCreate( ['tenant_id' => $tenantId, 'stat_date' => $date], [ 'order_count' => $orderCount, 'order_amount' => $orderAmount, // ... ] ); ``` --- ## 8. 참고 문서 | 문서 | 경로 | 용도 | |------|------|------| | DB 스키마 | `docs/specs/database-schema.md` | 원본 219개 테이블 구조 | | 시스템 아키텍처 | `docs/architecture/system-overview.md` | 전체 시스템 구조, 미들웨어, Docker | | API 규칙 | `docs/standards/api-rules.md` | Controller/Service 패턴, ApiResponse | | 품질 체크리스트 | `docs/standards/quality-checklist.md` | 코드 품질 검증 항목 | | 빠른 시작 | `docs/quickstart/quick-start.md` | 핵심 개발 규칙 3가지 | | Swagger 가이드 | `docs/guides/swagger-guide.md` | Swagger 작성 규칙 (Phase 4.4 시) | | Git 규칙 | `docs/standards/git-conventions.md` | 커밋 메시지 형식 | | 프로젝트 CLAUDE.md | `/SAM/CLAUDE.md` | 프로젝트 전체 규칙 및 맥락 | | API CLAUDE.md | `/SAM/api/CLAUDE.md` | API 저장소 상세 규칙 | --- ## 9. 자기완결성 점검 결과 | # | 검증 항목 | 상태 | 비고 | |---|----------|:----:|------| | 1 | 작업 목적이 명확한가? | ✅ | 섹션 1.1: sam_stat 별도 DB로 통계 분리 | | 2 | 성공 기준이 정의되어 있는가? | ✅ | 20개 테이블, 8 도메인, Phase별 검증 방법 명시 | | 3 | 작업 범위가 구체적인가? | ✅ | 섹션 4: 테이블별 DDL, 섹션 6: Phase별 구체적 작업 | | 4 | 의존성이 명시되어 있는가? | ✅ | 섹션 2.1: 원본 테이블 매핑, 섹션 0.2: DB 환경 | | 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 0.1, 0.3: 실제 파일 경로 검증됨 (2026-01-29) | | 6 | 단계별 절차가 실행 가능한가? | ✅ | Phase 1-5 구체적 작업 + bash 검증 커맨드 포함 | | 7 | 검증 방법이 명시되어 있는가? | ✅ | Phase 1, 2에 검증 bash 커맨드 블록 포함 | | 8 | 모호한 표현이 없는가? | ✅ | 파일 경로, 클래스명, 테이블명 모두 구체적 | ### 새 세션 시뮬레이션 테스트 | 질문 | 답변 가능 | 참조 섹션 | |------|:--------:|----------| | Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | | Q2. 어디서부터 시작해야 하는가? | ✅ | 0.9 체크리스트 → 6. Phase 1 | | Q3. 어떤 파일을 수정/생성해야 하는가? | ✅ | 0.1 프로젝트 구조 + 7.1~7.5 기술 설계 | | Q4. 기존 코드에 어떤 영향이 있는가? | ✅ | 0.3 기존 대시보드/보고서 시스템 | | Q5. DB 연결은 어떻게 설정하는가? | ✅ | 0.2 현재 DB 환경 + 7.1 Laravel 다중 DB | | Q6. 코딩 규칙은 무엇인가? | ✅ | 0.8 핵심 코딩 규칙 | | Q7. 작업 완료 확인 방법은? | ✅ | Phase 1, 2 검증 방법 블록 | | Q8. 스케줄러는 어떻게 등록하는가? | ✅ | 0.4 기존 스케줄러 패턴 + 7.4 | | Q9. Docker 환경은 어떻게 구성되어 있는가? | ✅ | 0.7 Docker 환경 | | Q10. 막혔을 때 참고 문서는? | ✅ | 8. 참고 문서 (9개 문서 매핑) | **결과**: 10/10 통과 → ✅ 자기완결성 확보 --- ## 10. 변경 이력 | 날짜 | 항목 | 내용 | |------|------|------| | 2026-01-29 | 초안 작성 | 프로젝트 분석 → 8개 도메인 도출 → 20개 테이블 설계 | | 2026-01-29 | 자기완결성 보완 | 섹션 0 추가 (프로젝트 컨텍스트, DB 환경, 기존 시스템, 코딩 규칙, 체크리스트) | | 2026-01-29 | 환경별 배포 구분 | 섹션 0.7 확장: 로컬(Docker) vs 개발서버(non-Docker) 구분, 배포 워크플로우 추가 | | 2026-01-29 | Phase 1 완료 | 인프라 구축: sam_stat DB 생성, 메타/dim_date 마이그레이션, 기반 모델 4개, 커맨드 2개, AggregatorService + Interface | | 2026-01-29 | Phase 2 완료 | P0 도메인: 매출/재무/생산 일간+월간 테이블 6개, 모델 6개, 서비스 3개, 스케줄러 2개 등록. 실데이터 집계 검증 완료 | | 2026-01-29 | Phase 3 완료 | P1 도메인: dim_client/dim_product 차원 + 재고/견적/인사 일간 3개 + KPI/알림 2개 = 테이블 7개, 모델 7개, 서비스 4개(Dimension/Inventory/Quote/Hr/KpiAlert), 커맨드 1개, 스케줄러 1개. 실데이터 검증 완료. products→items, client_groups.name→group_name 수정 | | 2026-01-29 | Phase 4 완료 | P2 도메인 + API + 대시보드: stat_project_monthly/stat_system_daily/stat_events/stat_snapshots 테이블 4개, 모델 4개, 서비스 4개(Project/System/StatEvent/StatQuery), StatController + FormRequest 3개 + routes/stats.php, StatEventObserver(6모델), DashboardService sam_stat 전환(폴백 패턴). 버그: whereHas→DB Builder 제거, User모델경로 수정. sam_stat 총 20테이블 | | 2026-01-29 | Phase 5 완료 | 최적화 및 안정화: StatBackfillCommand(백필), StatVerifyCommand(정합성 검증+자동 재집계), 파티셔닝 준비 마이그레이션(7테이블 RANGE), StatQueryService Redis 캐싱(TTL 5분+invalidateCache), StatMonitorService(집계 실패/누락/불일치 알림→stat_alerts), StatAggregatorService에 모니터링+캐시 무효화 연동. severity enum 수정(high→critical). 전체 테스트 통과 | | 2026-01-30 | Phase 6 완료 | 문서화 및 마무리: StatApi.php Swagger 문서(4 엔드포인트, 4 스키마), database-schema.md sam_stat 섹션 추가(20테이블+5커맨드+4API). 전체 6 Phase 100% 완료 | --- *이 문서는 /sc:plan 스킬로 생성되었습니다.*