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; } }