Files
HeuJung 1ba67ca7cf feat(content): 본문 이미지 목록 썸네일·og:image 폴백 전 티어 구현
공개 이슈 https://github.com/gnuboard/g7/issues/22 — 본문에 삽입한 이미지가
카드/갤러리 목록 썸네일과 공유 미리보기(og:image)에 반영되지 않던 공백을
코어+3모듈+2템플릿에서 해소.

- 코어 7.0.9: HtmlImageExtractor(내부 이미지 한정, origin 정규화 SSoT 재사용)
 + seo.og_image_default 사이트 기본 공유 이미지 설정 + SeoMetaResolver 폴백 체인
- board 1.1.0 / ecommerce 1.2.0 / page 1.1.0: content_thumbnail_url 캐시 컬럼,
 saving 추출(html 모드 한정·첨부 우선·비밀글 게이트 유지),
 filter_content_thumbnail 훅, 기존 데이터 백필 업그레이드 스텝
- ecommerce 카테고리 og:image 생산자 신설, page og:description 死키 정정
- sirsoft-basic 1.1.2 og 바인딩 정정, admin_basic 1.0.7 SEO 탭 업로더(+ja 팩 2종)
- 검수 중 발견한 text 모드 오캐시 결함은 실패 테스트 선행 후 수정
2026-08-22 22:21:54 +09:00

127 lines
4.1 KiB
PHP

<?php
namespace Modules\Sirsoft\Page\Tests\Feature\Upgrade;
use App\Extension\UpgradeContext;
use Illuminate\Support\Facades\DB;
use Modules\Sirsoft\Page\Tests\ModuleTestCase;
use Modules\Sirsoft\Page\Upgrades\Upgrade_1_1_0;
use PHPUnit\Framework\Attributes\Test;
/**
* 1.1.0 페이지 본문 썸네일 백필 업그레이드 스텝 테스트 (공개 이슈 #22 동종)
*
* @group page
* @group upgrade
*/
class PageContentThumbnailBackfillTest extends ModuleTestCase
{
protected function setUp(): void
{
parent::setUp();
config(['app.locale' => 'ko']);
}
protected function tearDown(): void
{
DB::table('pages')->where('slug', 'like', 'backfill-%')->delete();
parent::tearDown();
}
/**
* 모델 이벤트를 우회해 레거시 상태 페이지 행을 만듭니다.
*
* @param array $attributes 덮어쓸 속성
* @return int 페이지 ID
*/
private function createLegacyRow(array $attributes = []): int
{
return DB::table('pages')->insertGetId(array_merge([
'slug' => 'backfill-'.uniqid(),
'title' => json_encode(['ko' => '레거시 페이지'], JSON_UNESCAPED_UNICODE),
'content' => json_encode(['ko' => '<p>본문</p>'], JSON_UNESCAPED_UNICODE),
'content_mode' => 'html',
'content_thumbnail_url' => null,
'published' => 1,
'published_at' => now(),
'current_version' => 1,
'created_at' => now(),
'updated_at' => now(),
], $attributes));
}
private function runBackfill(): void
{
$context = new UpgradeContext('1.0.3', '1.1.0', '1.1.0', 'extension-upgrade');
(new Upgrade_1_1_0)->run($context);
}
/**
* @scenario backfill=legacy_row
*
* @effects backfill_fills_legacy_pages
*/
#[Test]
public function legacy_rows_are_backfilled_and_gated(): void
{
$legacy = $this->createLegacyRow([
'content' => json_encode([
'ko' => '<img src="https://evil.example.org/skip.jpg"><img src="/storage/pages/legacy.jpg">',
], JSON_UNESCAPED_UNICODE),
]);
$textMode = $this->createLegacyRow([
'content_mode' => 'text',
'content' => json_encode(['ko' => '텍스트 <img src="/storage/pages/literal.jpg">'], JSON_UNESCAPED_UNICODE),
]);
$filled = $this->createLegacyRow([
'content' => json_encode(['ko' => '<img src="/storage/pages/other.jpg">'], JSON_UNESCAPED_UNICODE),
'content_thumbnail_url' => '/storage/pages/kept.jpg',
]);
$this->runBackfill();
$this->assertSame('/storage/pages/legacy.jpg', DB::table('pages')->where('id', $legacy)->value('content_thumbnail_url'));
$this->assertNull(DB::table('pages')->where('id', $textMode)->value('content_thumbnail_url'));
$this->assertSame('/storage/pages/kept.jpg', DB::table('pages')->where('id', $filled)->value('content_thumbnail_url'));
}
/**
* @scenario backfill=idempotent
*
* @effects backfill_idempotent_second_run
*/
#[Test]
public function second_run_is_idempotent(): void
{
$this->createLegacyRow([
'content' => json_encode(['ko' => '<img src="/storage/pages/a.jpg">'], JSON_UNESCAPED_UNICODE),
]);
$this->createLegacyRow([
'content' => json_encode(['ko' => '<p>이미지 없음</p>'], JSON_UNESCAPED_UNICODE),
]);
$this->runBackfill();
$first = $this->snapshot();
$this->runBackfill();
$second = $this->snapshot();
$this->assertSame($first, $second, '재실행 결과가 동일해야 합니다.');
}
/**
* @return array<int, array{id: int, content_thumbnail_url: string|null}> 스냅샷
*/
private function snapshot(): array
{
return DB::table('pages')
->where('slug', 'like', 'backfill-%')
->orderBy('id')
->get(['id', 'content_thumbnail_url'])
->map(fn ($row) => ['id' => (int) $row->id, 'content_thumbnail_url' => $row->content_thumbnail_url])
->all();
}
}