Files
Gnuboard7/tests/Unit/Seo/SeoCacheSettingsTest.php
HeuJung 21f666e4b5 feat(seo): 사이트맵 스트리밍 생성 + SEO 캐시 설정 정합화
대용량 사이트에서 사이트맵 생성이 메모리 부족으로 반복 실패하던 문제(공개 )의
1단계로, 전체 URL을 메모리에 적재하던 구조를 스트리밍 분할 생성으로 교체한다.

사이트맵 스트리밍 코어:
- SitemapWriter — 자식 파일 1개 분량만 버퍼링하고 임계 도달 시 flush.
 StorageInterface가 append를 제공하지 않아(put이 전체 덮어쓰기) 택한 구조이며,
 최대 메모리가 "자식 파일 1개 크기"로 유계가 된다. URL 수(50,000)와
 바이트(45MB) 두 임계로 분할해 sitemaps.org 프로토콜을 지킨다.
- 커밋은 _tmp 전량 기록 → live 재배치 → manifest 마지막 기록 순. manifest 존재가
 커밋 완료 신호다. 기존 manifest를 먼저 지우지 않아 스왑 중 봇에게 503이 나가지 않는다.
- SitemapFileStore — 읽기측 + 파일 경로 SSoT. SitemapXmlRenderer — escape 단일 출처.

데이터 접근 계층 정리:
- contributor 3종이 쿼리를 직접 조립하던 것을 Repository 위임으로 전환.
 MySQL 버퍼드 쿼리에서 cursor는 결과셋 전체를 적재하므로 lazyById를 쓴다.
- SeoCacheStatsService → SeoCacheStatRepository 신설 (집계 4쿼리 → 1쿼리).
- audit 룰 service-direct-data-access의 사각 2건 수정 — 확장 디렉토리 정규식이
 상대경로를 매칭하지 못해 *Contributor처럼 접미사 없는 클래스가 전부 미검출이었다.

SEO 캐시 설정 정합화:
- SEO 설정의 캐시 3키가 cache 카테고리 형제 키에 항상 져서 운영자 입력이 조용히
 무시되고 있었다(설정을 cache 카테고리로 이관하며 SEO 탭 옛 칸을 미제거).
 고급 탭을 기준값으로 두되 SEO 탭에 지정이 있으면 오버라이드하도록 SeoCacheSettings
 단일 출처로 정리. "지정 여부"는 null로 판정하므로 기본값을 비웠다.
- 기존 설치는 Upgrade_7_1_0이 이행 — 옛 기본값과 같으면 미설정으로 비우고, 다르면
 운영자 의도로 보아 보존한다.

코어 버전 bump와 루트 CHANGELOG는 계획서 결정(D20)에 따라 마감 단계에서 일괄 수행한다.
2026-07-17 19:59:40 +09:00

163 lines
5.6 KiB
PHP

<?php
namespace Tests\Unit\Seo;
use App\Seo\SeoCacheSettings;
use Illuminate\Support\Facades\Config;
use Tests\TestCase;
/**
* SeoCacheSettings 단위 테스트 (결정 D19)
*
* 우선순위 규칙:
* 고급 탭(`cache.*`) = 메인 / SEO 탭(`seo.*`)에 별도 지정이 있으면 오버라이드.
* "별도 지정" 판정은 null 여부 — null 이면 미설정으로 보고 고급 탭 값을 따른다.
*
* 회귀 배경: 과거에는 `cache.*` 가 **항상** 이겨 SEO 탭 입력이 조용히 무시되었다.
* 이 저장소의 `seo.json` 에 `cache_ttl=3600` 이 저장돼 있었으나 실제로는
* `cache.seo_ttl=7200` 으로 동작하던 것이 실증 사례다.
*/
class SeoCacheSettingsTest extends TestCase
{
/**
* 테스트 초기화 - 두 카테고리를 모두 미설정 상태로 비웁니다.
*/
protected function setUp(): void
{
parent::setUp();
Config::set('g7_settings.core.seo', []);
Config::set('g7_settings.core.cache', []);
}
/**
* pageCacheTtl: SEO 탭이 미설정이면 고급 탭 값을 따른다
*/
public function test_page_cache_ttl_follows_advanced_tab_when_seo_is_unset(): void
{
Config::set('g7_settings.core.cache.seo_ttl', 7200);
Config::set('g7_settings.core.seo.cache_ttl', null);
$this->assertSame(7200, SeoCacheSettings::pageCacheTtl());
}
/**
* pageCacheTtl: SEO 탭에 값이 있으면 고급 탭을 오버라이드한다 (회귀 — 과거엔 무시됨)
*/
public function test_page_cache_ttl_seo_override_wins_over_advanced_tab(): void
{
Config::set('g7_settings.core.cache.seo_ttl', 7200);
Config::set('g7_settings.core.seo.cache_ttl', 3600);
$this->assertSame(
3600,
SeoCacheSettings::pageCacheTtl(),
'SEO 탭에 지정한 값이 고급 탭보다 우선해야 합니다.'
);
}
/**
* pageCacheTtl: 양쪽 다 미설정이면 최종 기본값
*/
public function test_page_cache_ttl_falls_back_to_default_when_both_unset(): void
{
$this->assertSame(7200, SeoCacheSettings::pageCacheTtl());
}
/**
* pageCacheEnabled: SEO 탭이 미설정이면 고급 탭 값을 따른다
*/
public function test_page_cache_enabled_follows_advanced_tab_when_seo_is_unset(): void
{
Config::set('g7_settings.core.cache.seo_enabled', false);
Config::set('g7_settings.core.seo.cache_enabled', null);
$this->assertFalse(SeoCacheSettings::pageCacheEnabled());
}
/**
* pageCacheEnabled: SEO 탭의 false 는 미설정이 아니라 오버라이드다
*
* null 이 아닌 falsy 값(false)을 "미설정" 으로 잘못 판정하면, 운영자가 SEO 캐시를
* 끄려 해도 고급 탭의 true 가 이겨 캐시가 계속 켜진다.
*/
public function test_page_cache_enabled_treats_false_as_explicit_override(): void
{
Config::set('g7_settings.core.cache.seo_enabled', true);
Config::set('g7_settings.core.seo.cache_enabled', false);
$this->assertFalse(
SeoCacheSettings::pageCacheEnabled(),
'false 는 미설정이 아니라 명시적 오버라이드여야 합니다.'
);
}
/**
* pageCacheEnabled: SEO 탭의 true 도 오버라이드로 동작한다
*/
public function test_page_cache_enabled_true_overrides_disabled_advanced_tab(): void
{
Config::set('g7_settings.core.cache.seo_enabled', false);
Config::set('g7_settings.core.seo.cache_enabled', true);
$this->assertTrue(SeoCacheSettings::pageCacheEnabled());
}
/**
* sitemapCacheTtl: SEO 탭이 미설정이면 고급 탭 값을 따른다
*/
public function test_sitemap_cache_ttl_follows_advanced_tab_when_seo_is_unset(): void
{
Config::set('g7_settings.core.cache.seo_sitemap_ttl', 86400);
Config::set('g7_settings.core.seo.sitemap_cache_ttl', null);
$this->assertSame(86400, SeoCacheSettings::sitemapCacheTtl());
}
/**
* sitemapCacheTtl: SEO 탭에 값이 있으면 고급 탭을 오버라이드한다 (회귀)
*/
public function test_sitemap_cache_ttl_seo_override_wins_over_advanced_tab(): void
{
Config::set('g7_settings.core.cache.seo_sitemap_ttl', 86400);
Config::set('g7_settings.core.seo.sitemap_cache_ttl', 3600);
$this->assertSame(3600, SeoCacheSettings::sitemapCacheTtl());
}
/**
* sitemapCacheTtl: 양쪽 다 미설정이면 최종 기본값
*/
public function test_sitemap_cache_ttl_falls_back_to_default_when_both_unset(): void
{
$this->assertSame(86400, SeoCacheSettings::sitemapCacheTtl());
}
/**
* 각 영역은 서로 독립이다 — SEO 페이지 캐시 지정이 sitemap 에 새지 않는다
*/
public function test_domains_are_independent(): void
{
Config::set('g7_settings.core.cache.seo_ttl', 7200);
Config::set('g7_settings.core.cache.seo_sitemap_ttl', 86400);
Config::set('g7_settings.core.seo.cache_ttl', 60);
$this->assertSame(60, SeoCacheSettings::pageCacheTtl());
$this->assertSame(
86400,
SeoCacheSettings::sitemapCacheTtl(),
'SEO 페이지 캐시 오버라이드가 sitemap 캐시에 영향을 주면 안 됩니다.'
);
}
/**
* 문자열로 저장된 값도 정수로 정규화된다 (폼 입력은 문자열로 올 수 있음)
*/
public function test_string_values_are_normalized_to_int(): void
{
Config::set('g7_settings.core.seo.sitemap_cache_ttl', '3600');
$this->assertSame(3600, SeoCacheSettings::sitemapCacheTtl());
}
}