baseUrl();
$today = date('Y-m-d');
$body = '' . PHP_EOL;
$body .= '' . PHP_EOL;
$body .= $this->sitemapEntry("{$baseUrl}/sitemap-pages.xml", $today);
for ($page = 1; $page <= $this->movieFileCount(); $page++) {
$body .= $this->sitemapEntry("{$baseUrl}/sitemap-movies-{$page}.xml", $today);
}
$body .= '' . PHP_EOL;
return $body;
});
return $this->response($xml);
}
/**
* 고정 페이지 + 게시판 목록 + 공개 게시글
*/
public function pages()
{
$xml = Cache::remember('sitemap.pages', self::CACHE_SECONDS, function () {
$baseUrl = $this->baseUrl();
$today = date('Y-m-d');
$body = $this->openUrlset();
// 로그인/회원가입/비밀번호 재설정은 색인 가치가 없어 제외한다.
$body .= $this->url("{$baseUrl}/", $today, 'daily', '1.0');
foreach (['rank', 'search', 'trailer', 'review', 'company'] as $section) {
$body .= $this->url("{$baseUrl}/movie/{$section}", $today, 'daily', '0.8');
}
$body .= $this->url("{$baseUrl}/tag", $today, 'daily', '0.6');
$boards = DB::table('tb_board')
->where('is_display', '=', 1)
->orderBy('sort')
->pluck('code');
foreach ($boards as $code) {
$body .= $this->url("{$baseUrl}/board/{$code}", $today, 'daily', '0.8');
}
// 공지/전체공지도 실제로 열리는 페이지이므로 함께 넣는다.
DB::table('tb_post')
->join('tb_board', 'tb_board.id', '=', 'tb_post.board_id')
->where('tb_board.is_display', '=', 1)
->where('tb_post.is_delete', '=', 0)
->where('tb_post.is_secret', '=', 0)
->where('tb_post.is_personal', '=', 0)
->select('tb_board.code', 'tb_post.id', 'tb_post.updated_at', 'tb_post.created_at')
->orderBy('tb_post.id')
->chunk(1000, function ($posts) use (&$body, $baseUrl, $today) {
foreach ($posts as $post) {
$modified = $post->updated_at ?: $post->created_at;
$lastModified = $modified ? date('Y-m-d', strtotime($modified)) : $today;
$body .= $this->url("{$baseUrl}/board/{$post->code}/{$post->id}", $lastModified, 'weekly', '0.6');
}
});
return $body . $this->closeUrlset();
});
return $this->response($xml);
}
/**
* 영화 상세. 페이지 번호는 1 부터 시작한다.
*/
public function movies(int $page)
{
if ($page < 1 || $page > $this->movieFileCount()) {
abort(404);
}
$xml = Cache::remember("sitemap.movies.{$page}", self::CACHE_SECONDS, function () use ($page) {
$baseUrl = $this->baseUrl();
$today = date('Y-m-d');
$body = $this->openUrlset();
// 뷰가 route('movie.search.show', base64_encode($row->movie_cd)) 로 링크를 만들므로 같은 규칙을 쓴다.
DB::table('tb_movie')
->select('movie_cd')
->orderBy('movie_cd')
->offset(($page - 1) * self::MOVIES_PER_FILE)
->limit(self::MOVIES_PER_FILE)
->get()
->each(function ($movie) use (&$body, $baseUrl, $today) {
if (!$movie->movie_cd) {
return;
}
$path = rawurlencode(base64_encode($movie->movie_cd));
$body .= $this->url("{$baseUrl}/movie/search/{$path}", $today, 'weekly', '0.7');
});
return $body . $this->closeUrlset();
});
return $this->response($xml);
}
/**
* 영화 sitemap 파일 개수. 최소 1 개는 내보낸다.
*/
private function movieFileCount(): int
{
$total = Cache::remember('sitemap.movies.total', self::CACHE_SECONDS, function () {
return DB::table('tb_movie')->count();
});
return max(1, (int)ceil($total / self::MOVIES_PER_FILE));
}
/**
* 레이아웃의 canonical(url()->current())과 같은 방식으로 요청 기준 주소를 쓴다.
* config('app.url') 은 환경파일 값에 좌우되어 어긋날 수 있다.
*/
private function baseUrl(): string
{
return rtrim(url('/'), '/');
}
private function response(string $xml)
{
return response($xml, 200)->header('Content-Type', 'application/xml; charset=utf-8');
}
private function openUrlset(): string
{
return '' . PHP_EOL
. '' . PHP_EOL;
}
private function closeUrlset(): string
{
return '' . PHP_EOL;
}
private function sitemapEntry(string $location, string $lastModified): string
{
return ' ' . PHP_EOL
. ' ' . htmlspecialchars($location, ENT_XML1) . '' . PHP_EOL
. ' ' . $lastModified . '' . PHP_EOL
. ' ' . PHP_EOL;
}
private function url(string $location, string $lastModified, string $changeFrequency, string $priority): string
{
return ' ' . PHP_EOL
. ' ' . htmlspecialchars($location, ENT_XML1) . '' . PHP_EOL
. ' ' . $lastModified . '' . PHP_EOL
. ' ' . $changeFrequency . '' . PHP_EOL
. ' ' . $priority . '' . PHP_EOL
. ' ' . PHP_EOL;
}
}