} * status: 'updated' | 'disabled' | 'failed' */ public function regenerate(SitemapGenerationMode $mode = SitemapGenerationMode::Auto, ?int $triggeredBy = null): array { $enabled = (bool) g7_core_settings('seo.sitemap_enabled', true); if (! $enabled) { return [ 'success' => false, 'status' => 'disabled', 'message' => __('seo.sitemap_disabled'), ]; } HookManager::doAction('core.seo.sitemap.before_regenerate'); try { $effectiveMode = $mode->resolve($this->repository->countVisible()); if ($effectiveMode === SitemapGenerationMode::Full) { $this->rebuildStore(); } $this->progress->writing($this->repository->countVisible()); $meta = $this->generator->generateToWriterFromEntries( $this->makeWriter(), $this->streamStoreEntries(), ); // 고급 탭(cache.seo_sitemap_ttl)이 메인, SEO 탭에 별도 지정이 있으면 그것이 우선 (D19) $ttl = SeoCacheSettings::sitemapCacheTtl(); // 디스크 서빙 전환 — 캐시에는 메타만 저장하고 구 XML 본문 캐시는 정리합니다. $this->cache->forget(self::LEGACY_XML_CACHE_KEY); $this->cache->put(self::META_CACHE_KEY, $meta, $ttl); $lastUpdatedAt = $meta['generated_at']; $this->updateLastUpdatedAt($lastUpdatedAt); $result = [ 'success' => true, 'status' => 'updated', 'message' => __('seo.sitemap_regenerated'), 'data' => [ 'last_updated_at' => $lastUpdatedAt, 'size_bytes' => $meta['size_bytes'], 'url_count' => $meta['url_count'], 'child_count' => $meta['child_count'], 'ttl' => $ttl, ], ]; $this->progress->complete($meta); HookManager::doAction('core.seo.sitemap.after_regenerate', $result, $triggeredBy); return $result; } catch (\Throwable $e) { Log::error('[SEO] Sitemap regeneration failed', [ 'error' => $e->getMessage(), ]); $this->progress->fail($e->getMessage()); $result = [ 'success' => false, 'status' => 'failed', 'message' => __('seo.sitemap_generate_failed', ['error' => $e->getMessage()]), ]; HookManager::doAction('core.seo.sitemap.after_regenerate_failed', $result, $triggeredBy); return $result; } } /** * 현재 sitemap 의 메타데이터와 진행상황을 반환합니다. * * realtime_enabled 는 설정 SSoT(drivers.websocket_enabled)와 실제 적용 config * (broadcasting.default) 를 함께 확인해 판정합니다 — 프론트가 실시간(구독)/폴링을 * 분기하는 근거입니다. * * @return array{last_updated_at: ?string, progress: ?array, realtime_enabled: bool} */ public function getStatus(): array { $lastUpdatedAt = (string) g7_core_settings('seo.sitemap_last_updated_at', ''); return [ 'last_updated_at' => $lastUpdatedAt !== '' ? $lastUpdatedAt : null, 'progress' => $this->progress->get(), 'realtime_enabled' => (bool) g7_core_settings('drivers.websocket_enabled', false) && config('broadcasting.default') !== 'null', ]; } /** * 각 기여자를 스트리밍하여 사이트맵 저장소를 전량 대체합니다(전체 재생성). * * 기여자별로 격리하여, 한 기여자가 실패해도 나머지 기여자의 재적재는 계속됩니다. */ private function rebuildStore(): void { // 스트림 삽입 건수를 누적해 running 단계의 "누적 URL 수" 를 표기합니다(D4). // countVisible() 같은 사전 count 쿼리 없이 삽입 결과만 합산하므로 1.4M 에서도 부담이 없습니다. $total = 0; foreach ($this->generator->getContributors() as $contributor) { try { // 현재 기여자 진입 시점의 누적치(직전 기여자들까지)를 표시합니다. $this->progress->phase($contributor->getIdentifier(), $total); $total += $this->repository->replaceAllForContributor( $contributor->getIdentifier(), $this->generator->streamContributorEntries($contributor), ); } catch (\Throwable $e) { Log::warning('[SEO] Sitemap 저장소 재적재 실패', [ 'contributor' => $contributor->getIdentifier(), 'error' => $e->getMessage(), ]); } } } /** * 사이트맵 저장소의 공개 URL 을 writer 항목 형태로 지연 스트리밍합니다. * * @return iterable writer 항목 순회자 */ private function streamStoreEntries(): iterable { foreach ($this->repository->streamVisible() as $row) { yield [ 'loc' => (string) $row->loc, 'lastmod' => $row->lastmod?->toW3cString(), 'changefreq' => $row->changefreq, 'priority' => $row->priority !== null ? (float) $row->priority : null, ]; } } /** * 설정값을 반영한 SitemapWriter 를 생성합니다. * * @return SitemapWriter 분할 기록기 */ private function makeWriter(): SitemapWriter { return new SitemapWriter( $this->storage, SitemapXmlRenderer::fromConfig(), (int) g7_core_settings('seo.sitemap_urls_per_file', SitemapWriter::HARD_URL_CAP), (bool) g7_core_settings('seo.sitemap_gzip', false), ); } /** * settings 의 seo 카테고리에 sitemap_last_updated_at 을 기록합니다. * * @param string $iso8601 ISO8601 형식 타임스탬프 */ private function updateLastUpdatedAt(string $iso8601): void { try { $current = $this->configRepository->getCategory('seo'); $current['sitemap_last_updated_at'] = $iso8601; $this->configRepository->saveCategory('seo', $current); } catch (\Throwable $e) { Log::warning('Sitemap last_updated_at 갱신 실패', ['error' => $e->getMessage()]); } } }