- 완료된 계획 문서 22개를 plans/archive/로 이동 - tracked 16개 (git mv): bending-lot-pipeline, docs-update, fcm-notification 등 - untracked 6개 (mv): bending-worklog, formula-engine, mng-item 등 - index_plans.md 전면 업데이트 - 진행중 44개 / 완료 37개 현황 반영 - 각 문서별 실제 진행률 기재 (0%~94%) - 카테고리별 재정리 (견적/생산/품목/문서/마이그레이션/시스템/UI) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
58 KiB
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 기존 스케줄러 패턴 (따라야 할 패턴)
// 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 모델 패턴 (따라야 할 패턴)
// 기존 모델 예시 - 멀티테넌트 + 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)
# docker/docker-compose.yml 내 MySQL 서비스
# Docker 내부 호스트: sam-mysql-1
# sam_stat DB는 같은 MySQL 인스턴스에 생성 (별도 서버 불필요)
# 로컬 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회 세팅이 필요하다.
# 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 핵심 코딩 규칙 (이 작업에 적용)
- Service-First: 비즈니스 로직 → Service, Controller는 DI + 호출만
- FormRequest: Controller에서 직접 검증 금지
- BelongsToTenant: 원본 모델만 적용, 통계 모델은 tenant_id WHERE 직접 사용
- i18n: 메시지는
__('message.xxx')형태 - ApiResponse:
use App\Helpers\ApiResponse;→ApiResponse::handle() - Swagger: 별도 파일
api/app/Swagger/v1/{Resource}Api.php에 작성 - 커밋: 사용자 승인 후에만 커밋 (자동 커밋 금지)
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 - 통계 정의 (메타데이터 드리븐)
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 - 집계 작업 이력
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 - 날짜 차원
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 - 고객 차원 (스냅샷)
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 - 제품 차원 (스냅샷)
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 - 매출/수주 일일 통계
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 - 재무 일일 통계
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 - 생산 일일 통계
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 - 재고 일일 통계
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 - 견적/영업 일일 통계
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 - 인사/근태 일일 통계
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 - 건설/프로젝트 월간 통계
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 - 시스템 일일 통계
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 - 매출 월간 요약
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 - 재무 월간 요약
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 - 생산 월간 요약
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 목표 설정
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 - 통계 기반 알림
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 - 실시간 이벤트 로그 (확장용)
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 - 상태 스냅샷 (특정 시점 전체 상태)
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 검증 방법:
# 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 검증 방법:
# 수동 집계 실행 (특정 날짜)
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 연결
// 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 스케줄러 구조
// 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)
// 멱등성 보장: 같은 날짜 재실행 시 덮어쓰기
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 스킬로 생성되었습니다.