| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197 |
- <?php
- namespace App\Http\Controllers;
- use Illuminate\Support\Facades\Cache;
- use Illuminate\Support\Facades\DB;
- /**
- * sitemap 생성
- *
- * 영화가 45,000 건을 넘어 단일 파일로는 규격 상한(파일당 50,000 URL / 50MB)에 걸린다.
- * 그래서 sitemap index 로 나눈다.
- *
- * /sitemap.xml index (자식 목록)
- * /sitemap-pages.xml 고정 페이지 + 게시판 목록 + 공개 게시글
- * /sitemap-movies-1.xml 영화 상세 (MOVIES_PER_FILE 건 단위)
- *
- * 공개 판정 조건은 Board 모델의 기존 공개 목록 쿼리와 동일하게 맞춘다.
- * (BRD.is_display = 1, PST.is_delete = 0, PST.is_secret = 0, PST.is_personal = 0)
- */
- class SitemapController extends Controller
- {
- private const CACHE_SECONDS = 3600;
- // 규격 상한(50,000)에 여유를 둔다.
- private const MOVIES_PER_FILE = 40000;
- /**
- * sitemap index
- */
- public function index()
- {
- $xml = Cache::remember('sitemap.index', self::CACHE_SECONDS, function () {
- $baseUrl = $this->baseUrl();
- $today = date('Y-m-d');
- $body = '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL;
- $body .= '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . 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 .= '</sitemapindex>' . 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 '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL
- . '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . PHP_EOL;
- }
- private function closeUrlset(): string
- {
- return '</urlset>' . PHP_EOL;
- }
- private function sitemapEntry(string $location, string $lastModified): string
- {
- return ' <sitemap>' . PHP_EOL
- . ' <loc>' . htmlspecialchars($location, ENT_XML1) . '</loc>' . PHP_EOL
- . ' <lastmod>' . $lastModified . '</lastmod>' . PHP_EOL
- . ' </sitemap>' . PHP_EOL;
- }
- private function url(string $location, string $lastModified, string $changeFrequency, string $priority): string
- {
- return ' <url>' . PHP_EOL
- . ' <loc>' . htmlspecialchars($location, ENT_XML1) . '</loc>' . PHP_EOL
- . ' <lastmod>' . $lastModified . '</lastmod>' . PHP_EOL
- . ' <changefreq>' . $changeFrequency . '</changefreq>' . PHP_EOL
- . ' <priority>' . $priority . '</priority>' . PHP_EOL
- . ' </url>' . PHP_EOL;
- }
- }
|