Files
Gnuboard7/app/Http/Controllers/Api/Admin/SeoCacheController.php
T
HeuJung 1c99547bc1 feat(seo): 사이트맵 안전 서빙·잡 견고화·분할 설정 + 캐시 설정 정합
봇 요청 스레드의 동기 생성을 제거하고 디스크 스트리밍 서빙으로 전환했다.
신선도가 만료되면 생성 잡만 예약하고 기존 세트(stale) 또는 503+Retry-After 로
응답해, 대용량에서 봇 요청이 OOM/타임아웃을 유발하던 경로를 끊었다.

- 잡: ShouldBeUnique(고정 uniqueId)+uniqueFor+retryUntil, timeout 300→1800, backoff
- 컨트롤러: 인덱스 stale/503 분기 + 자식 라우트(.xml/.xml.gz 동일 액션)
- 스케줄 withoutOverlapping, 관리자 재생성 큐 이관
- 설정 4키(분할 기준·압축·stale 서빙·수집기 상한) + 고급탭 Sitemap 캐시 짝
- SitemapManager 응답 메시지 다국어화

설정 저장 테스트가 실제 설정 파일(사이트명/언어 포함)을 덮어쓰던 사전결함을
TestCase 전역 격리로 차단하고, 두 탭이 같은 다국어 키를 공유해 한쪽에 틀린
한도를 안내하던 사전결함을 키 분리로 해소했다.
2026-07-18 11:32:11 +09:00

140 lines
4.5 KiB
PHP

<?php
namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Api\Base\AdminBaseController;
use App\Http\Requests\Admin\SeoCacheClearRequest;
use App\Jobs\GenerateSitemapJob;
use App\Seo\Contracts\SeoCacheManagerInterface;
use App\Seo\SeoCacheStatsService;
use App\Seo\SitemapManager;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
/**
* SEO 캐시 관리 컨트롤러
*
* SEO 캐시 통계 조회, 캐시 삭제, 워밍업, 캐시된 URL 목록을 제공합니다.
*/
class SeoCacheController extends AdminBaseController
{
public function __construct(
private SeoCacheStatsService $statsService,
private SeoCacheManagerInterface $cacheManager,
private SitemapManager $sitemapManager
) {
parent::__construct();
}
/**
* SEO 캐시 통계를 조회합니다.
*
* 전체 통계, 레이아웃별 통계, 모듈별 통계를 반환합니다.
*
* @return JsonResponse 캐시 통계 데이터를 포함한 JSON 응답
*/
public function stats(): JsonResponse
{
try {
$since = Carbon::now()->subDays(7);
$data = [
'overall' => $this->statsService->getStats($since),
'by_layout' => $this->statsService->getStatsByLayout($since),
'by_module' => $this->statsService->getStatsByModule($since),
];
return $this->success('messages.success', $data);
} catch (\Exception $e) {
return $this->error('messages.error_occurred', 500, $e->getMessage());
}
}
/**
* SEO 캐시를 삭제합니다.
*
* 레이아웃 또는 모듈 지정 시 해당 캐시만, 미지정 시 전체 캐시를 삭제합니다.
*
* @param SeoCacheClearRequest $request 캐시 삭제 요청
* @return JsonResponse 삭제 결과를 포함한 JSON 응답
*/
public function clearCache(SeoCacheClearRequest $request): JsonResponse
{
try {
$layout = $request->validated('layout');
if ($layout) {
$count = $this->cacheManager->invalidateByLayout($layout);
return $this->success('messages.success', ['cleared' => $count]);
}
$this->cacheManager->clearAll();
return $this->success('messages.success', ['cleared' => 'all']);
} catch (\Exception $e) {
return $this->error('messages.error_occurred', 500, $e->getMessage());
}
}
/**
* SEO 캐시 워밍업을 실행합니다.
*
* 모든 SEO 레이아웃을 사전 렌더링합니다. (Phase 5에서 구현 예정)
*
* @return JsonResponse 워밍업 시작 결과를 포함한 JSON 응답
*/
public function warmup(): JsonResponse
{
try {
// Phase 5의 SeoDeclarationCollector 구현 후 실제 워밍업 로직 추가 예정
return $this->success('messages.success', [
'status' => 'dispatched',
'message' => __('seo.warmup_dispatched'),
]);
} catch (\Exception $e) {
return $this->error('messages.error_occurred', 500, $e->getMessage());
}
}
/**
* Sitemap XML 재생성을 큐에 예약합니다.
*
* 대용량(수백만 URL) 사이트에서 요청 스레드 동기 생성은 메모리/타임아웃 붕괴를 일으키므로
* 큐 잡으로 위임합니다. 응답은 예약 시점의 상태이며, 실제 완료 시각은 이후 갱신됩니다.
*
* @return JsonResponse 예약 결과 JSON 응답
*/
public function regenerateSitemap(): JsonResponse
{
if (! (bool) g7_core_settings('seo.sitemap_enabled', true)) {
return $this->error('seo.sitemap_disabled', 400);
}
GenerateSitemapJob::dispatch();
return $this->success('seo.sitemap_regenerate_dispatched', [
'status' => 'dispatched',
...$this->sitemapManager->getStatus(),
]);
}
/**
* 캐시된 URL 목록을 조회합니다.
*
* SeoCacheManager의 인덱스에서 현재 캐시된 URL 목록을 반환합니다.
*
* @return JsonResponse 캐시된 URL 목록을 포함한 JSON 응답
*/
public function cachedUrls(): JsonResponse
{
try {
$urls = $this->cacheManager->getCachedUrls();
return $this->success('messages.success', ['urls' => $urls, 'count' => count($urls)]);
} catch (\Exception $e) {
return $this->error('messages.error_occurred', 500, $e->getMessage());
}
}
}