Files
Gnuboard7/app/Repositories/SitemapUrlRepository.php
T
HeuJung face022073 feat(seo): 사이트맵 재생성 진행상황 + 실시간/폴링 UI
관리자 "전체 재생성"을 큐(Full 모드)로 던지고 진행상황을 SEO 탭에서 확인한다.
Reverb ON 이면 방송 채널 구독으로 실시간 갱신, OFF 면 상태 API 폴링으로 폴백한다.

- SitemapProgress 진행상황 스토어(queued→running→writing→completed/failed,
 캐시 TTL 3600, 방송 스로틀) + SitemapManager.getStatus 확장(progress + realtime_enabled)
- GET /api/admin/seo/sitemap/status(core.settings.read) 신규
- 관리자 재생성을 항상 Full 모드로 큐잉 + 진행률 기록, Job 최종 실패 시 progress=failed 고정
- core.admin.seo.sitemap 방송 채널 인증(core.settings.read)
- SEO 탭 진행상황 UI(상태 배지/단계/누적 URL/실시간·폴링 안내) + 폴링 분기
- 재생성 중 누적 URL 표기(replaceAllForContributor 삽입 건수 반환 — count 쿼리 없이 스트림 누적)
- ja 언어팩 진행상황 UI 번역 동기화
2026-07-18 18:22:16 +09:00

180 lines
5.5 KiB
PHP

<?php
namespace App\Repositories;
use App\Contracts\Repositories\SitemapUrlRepositoryInterface;
use App\Models\SitemapUrl;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
/**
* 사이트맵 URL 저장소 구현체
*
* sitemap_urls 테이블을 통해 리소스 단위 증분 갱신과 스트리밍 조회를 수행합니다.
* loc 은 2048자라 unique 인덱스에 직접 넣지 못하므로 sha256 해시(loc_hash)를
* identity 로 사용합니다. lastmod 는 삽입 전 타임스탬프 문자열로 정규화합니다.
*/
class SitemapUrlRepository implements SitemapUrlRepositoryInterface
{
/**
* 청크 삽입 크기 (전체 재생성 시 유계 메모리 보장)
*/
private const INSERT_CHUNK = 1000;
/**
* 스트리밍 조회 청크 크기 (lazyById 키셋 페이징)
*/
private const STREAM_CHUNK = 1000;
/**
* {@inheritDoc}
*/
public function upsertForResource(string $type, string $id, array $entries): void
{
DB::transaction(function () use ($type, $id, $entries): void {
SitemapUrl::query()
->where('resource_type', $type)
->where('resource_id', $id)
->delete();
$now = now();
$rows = [];
foreach ($entries as $entry) {
$rows[] = $this->toRow(
$type,
$id,
(string) ($entry['contributor'] ?? ''),
$entry,
$now,
);
}
if ($rows !== []) {
SitemapUrl::insert($rows);
}
});
}
/**
* {@inheritDoc}
*/
public function removeForResource(string $type, string $id): void
{
SitemapUrl::query()
->where('resource_type', $type)
->where('resource_id', $id)
->delete();
}
/**
* {@inheritDoc}
*/
public function streamVisible(?string $contributor = null): iterable
{
$query = SitemapUrl::query()->where('is_visible', true);
if ($contributor !== null) {
$query->where('contributor', $contributor);
}
// lazyById 는 id 키셋 페이징이라 결과셋 전체를 메모리/드라이버 버퍼에 적재하지 않습니다.
return $query->lazyById(self::STREAM_CHUNK);
}
/**
* {@inheritDoc}
*/
public function countVisible(): int
{
return SitemapUrl::query()->where('is_visible', true)->count();
}
/**
* {@inheritDoc}
*/
public function replaceAllForContributor(string $contributor, iterable $entries): int
{
// 기여자 스코프 전량 제거 후 청크 삽입. 대용량(수백만) 삽입을 단일 트랜잭션으로
// 감싸면 락/메모리 부담이 크므로, 삭제 후 청크별 삽입으로 유계 메모리를 유지합니다.
SitemapUrl::query()->where('contributor', $contributor)->delete();
$now = now();
$batch = [];
$inserted = 0;
foreach ($entries as $entry) {
$batch[] = $this->toRow(
(string) ($entry['resource_type'] ?? 'unknown'),
isset($entry['resource_id']) && $entry['resource_id'] !== null
? (string) $entry['resource_id']
: null,
$contributor,
$entry,
$now,
);
if (count($batch) >= self::INSERT_CHUNK) {
SitemapUrl::insert($batch);
$inserted += count($batch);
$batch = [];
}
}
if ($batch !== []) {
SitemapUrl::insert($batch);
$inserted += count($batch);
}
return $inserted;
}
/**
* URL 항목을 sitemap_urls 삽입용 행으로 변환합니다.
*
* @param string $type 리소스 유형
* @param string|null $id 리소스 PK (문자열, 없으면 null)
* @param string $contributor 기여자 식별자
* @param array<string, mixed> $entry URL 항목 (loc/lastmod/changefreq/priority)
* @param Carbon $now 타임스탬프 기준값
* @return array<string, mixed> 삽입용 행
*/
private function toRow(string $type, ?string $id, string $contributor, array $entry, Carbon $now): array
{
$loc = (string) ($entry['loc'] ?? '');
return [
'resource_type' => $type,
'resource_id' => $id,
'loc' => $loc,
'loc_hash' => hash('sha256', $loc),
'lastmod' => $this->normalizeLastmod($entry['lastmod'] ?? null),
'changefreq' => $entry['changefreq'] ?? null,
'priority' => $entry['priority'] ?? null,
'contributor' => $contributor,
'is_visible' => true,
'created_at' => $now,
'updated_at' => $now,
];
}
/**
* lastmod 값을 DB 저장용 타임스탬프 문자열로 정규화합니다.
*
* insert() 는 모델 캐스팅을 우회하므로 W3C 문자열/Carbon 을 직접 파싱합니다.
*
* @param mixed $lastmod 원본 lastmod (문자열/Carbon/null)
* @return string|null 정규화된 'Y-m-d H:i:s' 또는 null
*/
private function normalizeLastmod(mixed $lastmod): ?string
{
if ($lastmod === null || $lastmod === '') {
return null;
}
try {
return Carbon::parse($lastmod)->format('Y-m-d H:i:s');
} catch (\Throwable) {
return null;
}
}
}