공개 (@Tuwasduliebst 제보) 대응. - ckeditor5: 미참조 이미지 정리 엔진 신설 — 참조 판정(hash + 파일명 2토큰, filter_reference_sources 훅으로 확장 콘텐츠 등록) + 정리 커맨드(기본 꺼짐 옵트인, 3중 게이트) + 업로드 관리 화면(권한 분리·참조 배지·개별/선택 삭제) + 기설치본 설정 백필 upgrade step - board: 임시 첨부 GC(상시) + 소프트삭제 첨부 영구 정리(옵트인) + 설정 UI, 저장 payload 누락(F4)·빈 temp 디렉토리 잔존(F5) 수정 - ecommerce: 임시 상품 이미지 GC + 카테고리 삭제 시 이미지 파일 미삭제(S4) 수정 - page: 임시 첨부 GC - 코어: 고아 첨부 정리 커맨드(옵트인, site_logo 보호) + 사이트 로고 교체 시 구 로고 재편입 차단(S6) + DB 백업 미구현 501 정직화(B3) + 플러그인 관리자 메뉴 설치·업데이트 자동 동기화(F1·F7) - 동반: 플러그인 설정 검증 오류 필드명 라벨화(F3), Toggle 노드 type 정정 수평 전개(marketing 1.0.2 bump·gdpr·ecommerce), ja 언어팩 4종, API 문서, hooks.md, 시나리오 매니페스트 8종, Playwright E2E 4종
198 lines
8.6 KiB
PHP
198 lines
8.6 KiB
PHP
<?php
|
|
|
|
use App\Extension\ModuleManager;
|
|
use App\Extension\PluginManager;
|
|
use App\Jobs\GenerateSitemapJob;
|
|
use Illuminate\Foundation\Inspiring;
|
|
use Illuminate\Support\Facades\Artisan;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Schedule;
|
|
|
|
Artisan::command('inspire', function () {
|
|
$this->comment(Inspiring::quote());
|
|
})->purpose('Display an inspiring quote');
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Scheduled Commands
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| 아래에서 애플리케이션의 스케줄된 작업을 정의합니다.
|
|
|
|
|
*/
|
|
|
|
// 30초마다 시스템 리소스 브로드캐스트 (대시보드 실시간 업데이트)
|
|
// withoutOverlapping(5): 5분 후 mutex 자동 만료 — 백그라운드 프로세스가 비정상 종료되어도
|
|
// 무한 락에 빠지지 않도록 보호 (기본값은 무한 락)
|
|
Schedule::command('dashboard:broadcast-resources')
|
|
->everyThirtySeconds()
|
|
->withoutOverlapping(5)
|
|
->runInBackground();
|
|
|
|
// 만료된 레이아웃 미리보기 정리 (30분마다)
|
|
Schedule::command('layout-previews:cleanup')->everyThirtyMinutes();
|
|
|
|
/*
|
|
| 만료 데이터 정리 (일 1회 새벽 배치, 다중 서버 1대만)
|
|
|
|
|
| 시각 해석 기준은 사이트 설정 시간대(`general.timezone`)다 —
|
|
| SettingsServiceProvider 가 `app.schedule_timezone` 을 세팅하고,
|
|
| Laravel 이 그 값을 Schedule 인스턴스 전체에 일괄 적용한다.
|
|
| 서머타임이 있는 시간대에서는 전환일에 특정 시각이 건너뛰거나 두 번 발생할 수 있으나,
|
|
| 아래 정리 배치는 모두 멱등이라 이중 실행이 데이터를 해치지 않고
|
|
| 건너뛴 날은 다음 날 배치가 흡수한다.
|
|
|
|
|
| 아래 begin/end 표식 사이가 정리 예약의 전부다. 등록 전수 회귀 테스트가 이 구간을
|
|
| 읽어 대상 목록을 도출하므로, 정리 예약을 추가할 때는 반드시 이 안에 둔다.
|
|
*/
|
|
// gc-schedules:begin
|
|
Schedule::command('sanctum:prune-expired', ['--hours' => 24])->dailyAt('04:00')->onOneServer();
|
|
Schedule::command('auth:clear-resets')->dailyAt('04:05')->onOneServer();
|
|
Schedule::command('queue:prune-failed')->dailyAt('04:10')->onOneServer();
|
|
Schedule::command('queue:prune-batches')->dailyAt('04:15')->onOneServer();
|
|
Schedule::command('notification:cleanup')->dailyAt('04:20')->onOneServer();
|
|
Schedule::command('ext-bundles:cleanup')->dailyAt('04:25')->onOneServer();
|
|
Schedule::command('seo:prune-stats')->dailyAt('04:30')->onOneServer();
|
|
Schedule::command('schedules:prune-history')->dailyAt('04:35')->onOneServer();
|
|
Schedule::command('identity:prune-logs')->dailyAt('04:40')->onOneServer();
|
|
Schedule::command('activity-log:prune')->dailyAt('04:45')->onOneServer();
|
|
Schedule::command('notification-log:prune')->dailyAt('04:50')->onOneServer();
|
|
// 사용자 파일을 파기하므로 기본 꺼짐 — 커맨드가 `--scheduled` 에서 설정을 false 폴백으로 재확인한다.
|
|
Schedule::command('attachments:prune-orphans --scheduled')->dailyAt('04:55')->onOneServer();
|
|
|
|
// 만료 시각이 지난 본인인증 challenge 상태 전환 (비파괴 — 물리 파기는 identity:prune-logs)
|
|
Schedule::command('identity:expire-challenges')->hourly()->onOneServer();
|
|
// gc-schedules:end
|
|
|
|
// 언어팩 업데이트 확인 (주 1회, GitHub 기반 언어팩 latest_version 갱신)
|
|
Schedule::command('language-pack:check-updates')
|
|
->weekly()
|
|
->runInBackground()
|
|
->withoutOverlapping(60);
|
|
|
|
// Sitemap 생성 스케줄
|
|
if (file_exists(base_path('.env'))) {
|
|
$sitemapEnabled = (bool) g7_core_settings('seo.sitemap_enabled', true);
|
|
if ($sitemapEnabled) {
|
|
$sitemapFrequency = g7_core_settings('seo.sitemap_schedule', 'daily');
|
|
$sitemapTime = g7_core_settings('seo.sitemap_schedule_time', '02:00');
|
|
|
|
// 대용량 생성은 스케줄 주기를 넘길 수 있으므로 이전 실행이 끝나기 전에는 겹쳐 돌지 않게 한다.
|
|
$sitemapScheduled = Schedule::job(new GenerateSitemapJob)
|
|
->onOneServer()
|
|
->withoutOverlapping(30);
|
|
|
|
match ($sitemapFrequency) {
|
|
'hourly' => $sitemapScheduled->hourly(),
|
|
'weekly' => $sitemapScheduled->weekly()->at($sitemapTime),
|
|
default => $sitemapScheduled->daily()->at($sitemapTime),
|
|
};
|
|
}
|
|
}
|
|
|
|
// GeoIP DB 정기 갱신 스케줄
|
|
// 마스터 토글 + 자동 업데이트 토글 + 라이선스 키가 모두 충족되어야 등록됨
|
|
if (file_exists(base_path('.env'))) {
|
|
$geoipMasterEnabled = (bool) g7_core_settings('geoip.feature_enabled', false);
|
|
$geoipAutoUpdate = (bool) g7_core_settings('geoip.auto_update_enabled', true);
|
|
$geoipLicenseKey = (string) g7_core_settings('geoip.license_key', '');
|
|
|
|
if ($geoipMasterEnabled && $geoipAutoUpdate && $geoipLicenseKey !== '') {
|
|
$geoipFrequency = (string) config('geoip.schedule.frequency', 'weekly');
|
|
$geoipTime = (string) config('geoip.schedule.time', '03:00');
|
|
|
|
$geoipScheduled = Schedule::command('geoip:update')
|
|
->onOneServer()
|
|
->withoutOverlapping(60);
|
|
|
|
// MaxMind는 주 2회(화/금) 갱신 — 수요일 다운로드로 충분
|
|
match ($geoipFrequency) {
|
|
'daily' => $geoipScheduled->dailyAt($geoipTime),
|
|
default => $geoipScheduled->weeklyOn(3, $geoipTime),
|
|
};
|
|
}
|
|
}
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Extension Scheduled Commands
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| 활성 모듈/플러그인의 getSchedules()에 정의된 스케줄 작업을 등록합니다.
|
|
| 확장 업데이트 후 큐 워커 재시작과 함께 새 스케줄이 즉시 반영됩니다.
|
|
|
|
|
*/
|
|
|
|
if (file_exists(base_path('.env'))) {
|
|
try {
|
|
// 모듈 스케줄 등록
|
|
$moduleManager = app(ModuleManager::class);
|
|
foreach ($moduleManager->getActiveModules() as $module) {
|
|
foreach ($module->getSchedules() as $config) {
|
|
if (empty($config['command']) || empty($config['schedule'])) {
|
|
continue;
|
|
}
|
|
|
|
$cmd = Schedule::command($config['command']);
|
|
|
|
// cron expression (공백 포함) vs Laravel 메서드명 분기
|
|
str_contains($config['schedule'], ' ')
|
|
? $cmd->cron($config['schedule'])
|
|
: $cmd->{$config['schedule']}();
|
|
|
|
if (isset($config['description'])) {
|
|
$cmd->description($config['description']);
|
|
}
|
|
|
|
// enabled_config: "identifier.setting_key" → module_setting()으로 조회
|
|
if (isset($config['enabled_config'])) {
|
|
$identifier = $module->getIdentifier();
|
|
$settingKey = $config['enabled_config'];
|
|
|
|
// "identifier.key" 형식이면 identifier 부분 제거하여 설정 키만 추출
|
|
if (str_starts_with($settingKey, $identifier.'.')) {
|
|
$settingKey = substr($settingKey, strlen($identifier) + 1);
|
|
}
|
|
|
|
$cmd->when(fn () => (bool) module_setting($identifier, $settingKey, true));
|
|
}
|
|
}
|
|
}
|
|
|
|
// 플러그인 스케줄 등록
|
|
$pluginManager = app(PluginManager::class);
|
|
foreach ($pluginManager->getActivePlugins() as $plugin) {
|
|
foreach ($plugin->getSchedules() as $config) {
|
|
if (empty($config['command']) || empty($config['schedule'])) {
|
|
continue;
|
|
}
|
|
|
|
$cmd = Schedule::command($config['command']);
|
|
|
|
str_contains($config['schedule'], ' ')
|
|
? $cmd->cron($config['schedule'])
|
|
: $cmd->{$config['schedule']}();
|
|
|
|
if (isset($config['description'])) {
|
|
$cmd->description($config['description']);
|
|
}
|
|
|
|
// enabled_config: "identifier.setting_key" → plugin_setting()으로 조회
|
|
if (isset($config['enabled_config'])) {
|
|
$identifier = $plugin->getIdentifier();
|
|
$settingKey = $config['enabled_config'];
|
|
|
|
// "identifier.key" 형식이면 identifier 부분 제거하여 설정 키만 추출
|
|
if (str_starts_with($settingKey, $identifier.'.')) {
|
|
$settingKey = substr($settingKey, strlen($identifier) + 1);
|
|
}
|
|
|
|
$cmd->when(fn () => (bool) plugin_setting($identifier, $settingKey, true));
|
|
}
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
Log::debug('확장 스케줄 등록 스킵', ['error' => $e->getMessage()]);
|
|
}
|
|
}
|