이슈 본연: API 레퍼런스의 미채움 마커 5종(실측 제외/TODO/필드 없음/대표 에러 없음)
1,194건을 코드에서 읽어 전수 채우고(0건), api:docgen 재생성이 사람이 채운 내용을
손실·열화시키던 멱등성 결함 4종(CRLF, 표 파이프 이스케이프, 에러 서술 보존, 중복
라우트명 키)을 근본 수정. --check 를 실측 제외와 드리프트가 구분되도록 재정의.
ParameterDescriber/ResourceFieldDescriber 에 leaf 폴백·SEO 스코프·공통 사전 확장.
파생 결함(문서 실측 중 발견): ResponseHelper 기본 메시지 키 6종이 존재하지 않는
messages.* 를 가리켜 응답 message 가 번역문 대신 키 문자열로 노출되던 문제를 common.*
으로 정정하고, 코어 7 + gdpr/marketing/pay_kginicis/verification/ecommerce 확장의
호출부와 누락 lang 키(게시판 10키 + 이커머스 category_images 4키 포함)를 ko/en/ja
전수 정정. 중복 그룹 키 2건은 기존 키로 호출부 통합.
보안: GET /api/identity/challenges/{id} 는 권한 가드 없는 공개 폴링 엔드포인트인데
Service::getStatus 가 attempts/max_attempts 를 응답에 담아 남은 시도 횟수를 추론할 수
있었다. 두 필드를 제거하고, 챌린지 화면(admin_basic/basic)의 서버 attempts 참조를
제거(남은 횟수 UI 는 query fallback + verify 실패 시 로컬 증가로 유지).
부수 정리(변경셋에 들어온 board 컨트롤러의 audit 사전 결함): FormRequest 4개 신설로
base Request 주입 제거, CommentController 의 Board 직접 호출을 BoardService 위임으로
전환, PHPDoc @return 보강. board 스위트에서 발견한 stale test 3건(seed SSoT 불일치,
admin 계정 user 역할 누락)도 같은 세션에서 정정.
버전 정렬(공개 release 기준): 코어 7.0.3→7.0.4, sirsoft-board 1.0.1→1.0.2,
verification_kginicis 1.0.0→1.0.1, admin_basic 1.0.1→1.0.4.
158 lines
5.5 KiB
PHP
158 lines
5.5 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api\Admin;
|
|
|
|
use App\Enums\SitemapGenerationMode;
|
|
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 App\Seo\SitemapProgress;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Support\Facades\Auth;
|
|
|
|
/**
|
|
* SEO 캐시 관리 컨트롤러
|
|
*
|
|
* SEO 캐시 통계 조회, 캐시 삭제, 워밍업, 캐시된 URL 목록을 제공합니다.
|
|
*/
|
|
class SeoCacheController extends AdminBaseController
|
|
{
|
|
public function __construct(
|
|
private SeoCacheStatsService $statsService,
|
|
private SeoCacheManagerInterface $cacheManager,
|
|
private SitemapManager $sitemapManager,
|
|
private SitemapProgress $sitemapProgress
|
|
) {
|
|
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('common.success', $data);
|
|
} catch (\Exception $e) {
|
|
return $this->error('common.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('common.success', ['cleared' => $count]);
|
|
}
|
|
|
|
$this->cacheManager->clearAll();
|
|
|
|
return $this->success('common.success', ['cleared' => 'all']);
|
|
} catch (\Exception $e) {
|
|
return $this->error('common.error_occurred', 500, $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* SEO 캐시 워밍업을 실행합니다.
|
|
*
|
|
* 모든 SEO 레이아웃을 사전 렌더링합니다. (Phase 5에서 구현 예정)
|
|
*
|
|
* @return JsonResponse 워밍업 시작 결과를 포함한 JSON 응답
|
|
*/
|
|
public function warmup(): JsonResponse
|
|
{
|
|
try {
|
|
// Phase 5의 SeoDeclarationCollector 구현 후 실제 워밍업 로직 추가 예정
|
|
return $this->success('common.success', [
|
|
'status' => 'dispatched',
|
|
'message' => __('seo.warmup_dispatched'),
|
|
]);
|
|
} catch (\Exception $e) {
|
|
return $this->error('common.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);
|
|
}
|
|
|
|
// 관리자 수동 재생성은 항상 전체(Full) — 현 생성 상태와 무관하게 전량 재생성 (D7)
|
|
// 실행한 관리자 ID 를 함께 실어, 완료/실패 시 그 관리자에게만 알림이 발송되게 한다.
|
|
GenerateSitemapJob::dispatch(SitemapGenerationMode::Full, Auth::id());
|
|
|
|
// 큐 대기 중에도 UI 가 'queued' 를 표시하도록 즉시 기록
|
|
$this->sitemapProgress->start(SitemapGenerationMode::Full->value);
|
|
|
|
return $this->success('seo.sitemap_regenerate_dispatched', $this->sitemapManager->getStatus());
|
|
}
|
|
|
|
/**
|
|
* Sitemap 재생성 진행상황과 실시간 연결 가능 여부를 조회합니다.
|
|
*
|
|
* SEO 탭 진입 시 초기 로드용이며, 폴링(Reverb OFF)일 때 주기적으로 재조회됩니다.
|
|
*
|
|
* @return JsonResponse 진행상황(progress) + last_updated_at + realtime_enabled 를 포함한 JSON 응답
|
|
*/
|
|
public function sitemapStatus(): JsonResponse
|
|
{
|
|
return $this->success('messages.success', $this->sitemapManager->getStatus());
|
|
}
|
|
|
|
/**
|
|
* 캐시된 URL 목록을 조회합니다.
|
|
*
|
|
* SeoCacheManager의 인덱스에서 현재 캐시된 URL 목록을 반환합니다.
|
|
*
|
|
* @return JsonResponse 캐시된 URL 목록을 포함한 JSON 응답
|
|
*/
|
|
public function cachedUrls(): JsonResponse
|
|
{
|
|
try {
|
|
$urls = $this->cacheManager->getCachedUrls();
|
|
|
|
return $this->success('common.success', ['urls' => $urls, 'count' => count($urls)]);
|
|
} catch (\Exception $e) {
|
|
return $this->error('common.error_occurred', 500, $e->getMessage());
|
|
}
|
|
}
|
|
}
|