feat(seo): 사이트맵 증분 저장소 + 리소스 리스너 재배선

사이트맵을 리소스 단위로 영속화(sitemap_urls)해 진정한 증분 갱신과
도메인 재쿼리 없는 파일 재작성을 구현했다. 리소스 공개/비공개/삭제 시
해당 항목만 색인이 갱신되고, 관리자=full·스케줄러=auto·리소스 변경=증분으로 동작한다.

- sitemap_urls 마이그레이션 + SitemapUrl 모델. loc(2048) 를 unique 에 직접 넣으면
 InnoDB 키 길이를 초과하므로 loc_hash(sha256) 컬럼을 identity 로 사용
- SitemapUrlRepository(멱등 upsert / remove / 스트리밍) + SitemapIndexer
 (+ 제3자용 sitemap.index.collect_for_resource 필터 훅)
- 4 리스너 재배선: 공개→색인 / 비공개·삭제→색인 해제 + 재생성 잡 디바운스 디스패치.
 3 기여자가 resource_type/resource_id 를 함께 emit 해 전체·증분 identity 를 일치
- 재생성 모드를 SitemapGenerationMode Backed Enum 으로 통일(Command→Job→Manager)
- seo:generate-sitemap 에 --rebuild / --mode 옵션 추가
This commit is contained in:
HeuJung committed 2026-07-18 16:19:49 +09:00
1 parent 1a6fca0d6b
commit 1629684156
34 files changed
+2027 -23

No files matched your search

+26 -2
View File
@@ -4,12 +4,14 @@ namespace Tests\Unit\Seo;
use App\Contracts\Extension\CacheInterface;
use App\Contracts\Repositories\ConfigRepositoryInterface;
use App\Contracts\Repositories\SitemapUrlRepositoryInterface;
use App\Extension\HookManager;
use App\Extension\Storage\CoreStorageDriver;
use App\Seo\Contracts\SitemapContributorInterface;
use App\Seo\SitemapFileStore;
use App\Seo\SitemapGenerator;
use App\Seo\SitemapManager;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Storage;
use Mockery;
@@ -31,6 +33,8 @@ use Tests\TestCase;
*/
class SitemapManagerTest extends TestCase
{
use RefreshDatabase;
/**
* 이 테스트가 등록·검증하는 sitemap 훅 목록
*/
@@ -128,6 +132,7 @@ class SitemapManagerTest extends TestCase
$cache ?? Mockery::spy(CacheInterface::class),
$config ?? Mockery::spy(ConfigRepositoryInterface::class),
$this->storage,
$this->app->make(SitemapUrlRepositoryInterface::class),
);
}
@@ -299,13 +304,14 @@ class SitemapManagerTest extends TestCase
// 생성 도중 예외를 던지는 generator 로 실패를 강제한다.
$generator = Mockery::mock(SitemapGenerator::class);
$generator->shouldReceive('generateToWriter')->andThrow(new \RuntimeException('디스크 장애'));
$generator->shouldReceive('generateToWriterFromEntries')->andThrow(new \RuntimeException('디스크 장애'));
$manager = new SitemapManager(
$generator,
Mockery::spy(CacheInterface::class),
Mockery::spy(ConfigRepositoryInterface::class),
$this->storage,
$this->incrementalRepository(),
);
$result = $manager->regenerate();
@@ -329,18 +335,36 @@ class SitemapManagerTest extends TestCase
$cache->shouldNotReceive('forget');
$generator = Mockery::mock(SitemapGenerator::class);
$generator->shouldReceive('generateToWriter')->andThrow(new \RuntimeException('장애'));
$generator->shouldReceive('generateToWriterFromEntries')->andThrow(new \RuntimeException('장애'));
$manager = new SitemapManager(
$generator,
$cache,
Mockery::spy(ConfigRepositoryInterface::class),
$this->storage,
$this->incrementalRepository(),
);
$this->assertFalse($manager->regenerate()['success']);
}
/**
* incremental 모드로 진입시키는(테이블이 비어있지 않은) mock 저장소를 만듭니다.
*
* countVisible()>0 → auto 가 incremental 로 해석되어 rebuildStore(전체 재적재)를 건너뛰고
* 곧바로 generateToWriterFromEntries 로 진입합니다(실패 경로 단순화).
*
* @return SitemapUrlRepositoryInterface mock 저장소
*/
private function incrementalRepository(): SitemapUrlRepositoryInterface
{
$repository = Mockery::mock(SitemapUrlRepositoryInterface::class);
$repository->shouldReceive('countVisible')->andReturn(1);
$repository->shouldReceive('streamVisible')->andReturn([]);
return $repository;
}
/**
* regenerate: seo.sitemap_urls_per_file 설정이 자식 분할에 반영된다
*/