feat: sam_stat 통계 DB 인프라 구축 (Phase 1)
- sam_stat DB 연결 설정 (config/database.php, .env) - 메타 테이블 마이그레이션 (stat_definitions, stat_job_logs) - dim_date 차원 테이블 + DimDateSeeder (2020~2030, 4018건) - 기반 모델: BaseStatModel, StatDefinition, StatJobLog, DimDate - 집계 커맨드: stat:aggregate-daily, stat:aggregate-monthly - StatAggregatorService + StatDomainServiceInterface 골격 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection($this->connection)->create('stat_definitions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('code', 100)->unique()->comment('통계 코드 (sales_daily_revenue)');
|
||||
$table->string('domain', 50)->index()->comment('도메인 (sales, finance, production)');
|
||||
$table->string('name', 200)->comment('통계명 (일일 매출액)');
|
||||
$table->text('description')->nullable();
|
||||
$table->json('source_tables')->comment('원본 테이블 목록');
|
||||
$table->string('aggregation', 20)->default('daily')->index()->comment('집계 주기');
|
||||
$table->text('query_template')->nullable()->comment('집계 SQL 템플릿');
|
||||
$table->boolean('is_active')->default(true)->index();
|
||||
$table->json('config')->nullable()->comment('추가 설정 (임계값, 단위 등)');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection($this->connection)->dropIfExists('stat_definitions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection($this->connection)->create('stat_job_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
|
||||
$table->string('job_type', 100)->comment('작업 유형 (sales_daily, finance_monthly)');
|
||||
$table->date('target_date')->comment('집계 대상 날짜');
|
||||
$table->enum('status', ['pending', 'running', 'completed', 'failed'])->default('pending');
|
||||
$table->unsignedInteger('records_processed')->default(0);
|
||||
$table->text('error_message')->nullable();
|
||||
$table->timestamp('started_at')->nullable();
|
||||
$table->timestamp('completed_at')->nullable();
|
||||
$table->unsignedInteger('duration_ms')->nullable();
|
||||
$table->timestamp('created_at')->nullable();
|
||||
|
||||
$table->index(['tenant_id', 'job_type']);
|
||||
$table->index('status');
|
||||
$table->index('target_date');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection($this->connection)->dropIfExists('stat_job_logs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection($this->connection)->create('dim_date', function (Blueprint $table) {
|
||||
$table->date('date_key')->primary()->comment('날짜 키');
|
||||
$table->smallInteger('year')->comment('연도');
|
||||
$table->tinyInteger('quarter')->comment('분기 (1~4)');
|
||||
$table->tinyInteger('month')->comment('월');
|
||||
$table->tinyInteger('week')->comment('ISO 주차');
|
||||
$table->tinyInteger('day_of_week')->comment('요일 (1:월~7:일)');
|
||||
$table->tinyInteger('day_of_month')->comment('일');
|
||||
$table->boolean('is_weekend')->comment('주말 여부');
|
||||
$table->boolean('is_holiday')->default(false)->comment('공휴일 여부');
|
||||
$table->string('holiday_name', 100)->nullable()->comment('공휴일명');
|
||||
$table->smallInteger('fiscal_year')->nullable()->comment('회계연도');
|
||||
$table->tinyInteger('fiscal_quarter')->nullable()->comment('회계분기');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection($this->connection)->dropIfExists('dim_date');
|
||||
}
|
||||
};
|
||||
81
database/seeders/DimDateSeeder.php
Normal file
81
database/seeders/DimDateSeeder.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonPeriod;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DimDateSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$start = Carbon::create(2020, 1, 1);
|
||||
$end = Carbon::create(2030, 12, 31);
|
||||
$period = CarbonPeriod::create($start, $end);
|
||||
|
||||
$holidays = $this->getKoreanHolidays();
|
||||
|
||||
$batch = [];
|
||||
$batchSize = 500;
|
||||
|
||||
foreach ($period as $date) {
|
||||
$dateStr = $date->format('Y-m-d');
|
||||
$holiday = $holidays[$dateStr] ?? null;
|
||||
|
||||
$batch[] = [
|
||||
'date_key' => $dateStr,
|
||||
'year' => $date->year,
|
||||
'quarter' => $date->quarter,
|
||||
'month' => $date->month,
|
||||
'week' => (int) $date->isoWeek(),
|
||||
'day_of_week' => $date->dayOfWeekIso,
|
||||
'day_of_month' => $date->day,
|
||||
'is_weekend' => $date->isWeekend(),
|
||||
'is_holiday' => $holiday !== null,
|
||||
'holiday_name' => $holiday,
|
||||
'fiscal_year' => $date->year,
|
||||
'fiscal_quarter' => $date->quarter,
|
||||
];
|
||||
|
||||
if (count($batch) >= $batchSize) {
|
||||
DB::connection('sam_stat')->table('dim_date')->insert($batch);
|
||||
$batch = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($batch)) {
|
||||
DB::connection('sam_stat')->table('dim_date')->insert($batch);
|
||||
}
|
||||
|
||||
$totalCount = DB::connection('sam_stat')->table('dim_date')->count();
|
||||
$this->command->info("dim_date 시딩 완료: {$totalCount}건 (2020-01-01 ~ 2030-12-31)");
|
||||
}
|
||||
|
||||
/**
|
||||
* 한국 공휴일 목록 (고정 공휴일만, 음력 공휴일은 수동 추가 필요)
|
||||
*/
|
||||
private function getKoreanHolidays(): array
|
||||
{
|
||||
$holidays = [];
|
||||
|
||||
for ($year = 2020; $year <= 2030; $year++) {
|
||||
// 고정 공휴일
|
||||
$fixed = [
|
||||
"{$year}-01-01" => '신정',
|
||||
"{$year}-03-01" => '삼일절',
|
||||
"{$year}-05-05" => '어린이날',
|
||||
"{$year}-06-06" => '현충일',
|
||||
"{$year}-08-15" => '광복절',
|
||||
"{$year}-10-03" => '개천절',
|
||||
"{$year}-10-09" => '한글날',
|
||||
"{$year}-12-25" => '크리스마스',
|
||||
];
|
||||
|
||||
$holidays = array_merge($holidays, $fixed);
|
||||
}
|
||||
|
||||
return $holidays;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user