Browse Source

feat(movie): 영화사 페이지 DB 조회 전환 + 캐시, TrustProxies 프록시 추가

- Company Eloquent 모델 신설 (tb_company/tb_company_filmo 조회)
- CompanyController: crawler API 매요청 호출 → DB 우선 + 5분 캐시, 미수집 시 API 폴백
- TrustProxies: 192.168.0.201 추가

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
admin@web.or.kr 2 ngày trước cách đây
mục cha
commit
2f037aaf4b

+ 56 - 43
app/Http/Controllers/Movie/CompanyController.php

@@ -5,11 +5,14 @@ namespace App\Http\Controllers\Movie;
 use Illuminate\Http\Request;
 use Illuminate\Http\Client\ConnectionException;
 use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\Validator;
 use App\Http\Controllers\Controller;
 use App\Http\Traits\CommonTrait;
 use App\Http\Traits\PagingTrait;
+use App\Models\DTO\SearchData;
 use App\Models\DTO\Movie\CompanyParams;
+use App\Models\Movie\Company;
 use Exception;
 
 class CompanyController extends Controller
@@ -18,19 +21,21 @@ class CompanyController extends Controller
     use PagingTrait;
 
     private string $host;
+    private Company $companyModel;
 
-    public function __construct() {
+    public function __construct(Company $companyModel) {
         $this->middleware('front');
 
         $this->host = env('MOVIE_API');
+        $this->companyModel = $companyModel;
     }
 
     /**
-     * 영화사 목록
+     * 영화사 목록 (DB 조회)
      * @method GET
      * @see /movie/company
      */
-    public function index(Request $request, CompanyParams $params)
+    public function index(Request $request)
     {
         $rules = [
             'page' => 'nullable|numeric',
@@ -53,51 +58,32 @@ class CompanyController extends Controller
         // 검색 유효성 검사
         Validator::make($request->all(), $rules, $message, $attributes)->validated();
 
-        $page = max(1, (int)$request->get('page', 1));
-        $params->curPage = $page;
-        $params->companyNm = $request->get('keyword');
-        $params->ceoNm = $request->get('ceo');
+        $params = SearchData::fromRequest($request);
+        $data = $this->companyModel->list($params);
 
-        // 검색 조건 (crawler 장애 시 빈 목록으로 격리)
-        $total = 0;
-        $list = [];
-        try {
-            $response = Http::timeout(5)->get($this->host . COMPANY_LIST, $params->toArray());
-        } catch (ConnectionException) {
-            $response = null;
-        }
-
-        if ($response && $response->ok()) {
-            $data = json_decode($response->body());
-            $total = (int)($data->total ?? 0);
-            if ($total > 0) {
-                $queryString = $request->getQueryString();
-                foreach ($data->list as $row) {
-                    $row->viewURL = route('movie.company.show', base64_encode($row->companyCd));
-                    if ($queryString) {
-                        $row->viewURL .= ('?' . $queryString);
-                    }
-                    $list[] = $row;
-                }
+        // 상세 보기 URL 세팅
+        $queryString = $request->getQueryString();
+        foreach ($data->list as $row) {
+            $row->viewURL = route('movie.company.show', base64_encode($row->companyCd));
+            if ($queryString) {
+                $row->viewURL .= ('?' . $queryString);
             }
         }
 
-        $companies = $this->getPaginator($list, $total, $params->itemPerPage, $page);
-
         return view(layout('movie.company.index'), [
             'keyword' => $request->get('keyword'),
             'ceo' => $request->get('ceo'),
-            'total' => $total,
-            'companies' => $companies
+            'total' => $data->total,
+            'companies' => $data->list
         ]);
     }
 
     /**
-     * 영화사 상세 보기
+     * 영화사 상세 보기 (DB 우선 + 캐시, 미수집 시 API 폴백)
      * @method GET
      * @see /movie/company/{company_cd}
      */
-    public function show(string $base64, Request $request, CompanyParams $params)
+    public function show(string $base64, Request $request)
     {
         try {
 
@@ -116,16 +102,22 @@ class CompanyController extends Controller
                 $listURL .= ('?' . $queryString);
             }
 
-            // 검색 조건
-            $params->companyCd = $companyCd;
-            $response = Http::timeout(5)->get($this->host . COMPANY_INFO, $params->toArray());
+            // 캐시 → DB → crawler API 폴백 순으로 조회
+            $cacheKey = ('movie:company:' . $companyCd);
+            $info = Cache::get($cacheKey);
 
-            if(!$response->ok()) {
-                throw new Exception('요청한 정보가 없습니다.');
-            }
+            if (!$info) {
+                $info = $this->companyModel->info($companyCd);
 
-            $data = json_decode($response->body());
-            $info = ($data->info ?? null);
+                // DB 미수집이면 crawler API 폴백 (수집은 cron 이 채움)
+                if (!$info) {
+                    $info = $this->fetchFromApi($companyCd);
+                }
+
+                if ($info && !empty($info->companyCd)) {
+                    Cache::put($cacheKey, $info, CACHE_EXPIRE_TIME);
+                }
+            }
 
             if(!$info || empty($info->companyCd)) {
                 throw new Exception('요청한 정보가 없습니다.');
@@ -142,8 +134,29 @@ class CompanyController extends Controller
                 'listURL' => $listURL
             ]);
 
-        }catch(Exception $e) {
+        } catch(Exception $e) {
             abort($e->getCode() ?: 500, $e->getMessage());
         }
     }
+
+    /**
+     * DB 미수집 영화사를 crawler API 로 즉석 조회 (폴백)
+     */
+    private function fetchFromApi(string $companyCd): ?object
+    {
+        try {
+            $params = new CompanyParams();
+            $params->companyCd = $companyCd;
+            $response = Http::timeout(5)->get($this->host . COMPANY_INFO, $params->toArray());
+        } catch (ConnectionException) {
+            return null;
+        }
+
+        if (!$response->ok()) {
+            return null;
+        }
+
+        $data = json_decode($response->body());
+        return ($data->info ?? null);
+    }
 }

+ 1 - 1
app/Http/Middleware/TrustProxies.php

@@ -12,7 +12,7 @@ class TrustProxies extends Middleware
      *
      * @var array<int, string>|string|null
      */
-    protected $proxies = '172.18.0.0/16';
+    protected $proxies = ['192.168.0.201', '172.18.0.0/16'];
 
     /**
      * The headers that should be used to detect proxies.

+ 138 - 0
app/Models/Movie/Company.php

@@ -0,0 +1,138 @@
+<?php
+
+namespace App\Models\Movie;
+
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Facades\DB;
+use App\Models\DTO\SearchData;
+use App\Http\Traits\PagingTrait;
+use App\Http\Traits\CommonTrait;
+
+class Company extends Model
+{
+    use CommonTrait;
+    use PagingTrait;
+
+    protected $table = 'tb_company';
+    protected $primaryKey = 'id';
+
+    public $keyType = 'int';
+    public $incrementing = true;
+    public $timestamps = true;
+
+    const CREATED_AT = 'created_at';
+    const UPDATED_AT = 'updated_at';
+    const DELETED_AT = null;
+
+    protected $guarded = [];
+
+    /**
+     * 영화사 목록 조회 (검색 + 페이징)
+     */
+    public function list(SearchData $params): object
+    {
+        $where = $this->buildWhereQuery($params);
+
+        $sql = sprintf("
+            SELECT
+                COUNT(*) AS total
+            FROM tb_company CMP
+            WHERE TRUE %s;
+        ", $where);
+
+        $total = DB::selectOne($sql)->total;
+
+        $sql = sprintf("
+            SELECT
+                CMP.company_cd AS companyCd,
+                CMP.company_nm AS companyNm,
+                CMP.company_nm_en AS companyNmEn,
+                CMP.ceo_nm AS ceoNm,
+                CMP.company_part_names AS companyPartNames,
+                CMP.filmo_names AS filmoNames
+            FROM tb_company CMP
+            WHERE TRUE %s
+            ORDER BY
+                CMP.company_nm ASC
+            LIMIT ?, ?;
+        ", $where);
+
+        $list = $this->getPaginator(
+            DB::select($sql, [$params->offset, $params->perPage]),
+            $total, $params->perPage, $params->page, $params->path
+        );
+
+        $rows = $list->count();
+
+        return (object)[
+            'total' => $total,
+            'rows' => $rows,
+            'list' => $list
+        ];
+    }
+
+    /**
+     * 영화사 상세 조회 (기본정보 + 분류(parts) + 필모그래피)
+     */
+    public function info(string $companyCd): ?object
+    {
+        $sql = "
+            SELECT
+                company_cd AS companyCd,
+                company_nm AS companyNm,
+                company_nm_en AS companyNmEn,
+                ceo_nm AS ceoNm,
+                parts
+            FROM tb_company
+            WHERE company_cd = ?
+            LIMIT 1;
+        ";
+
+        $row = DB::selectOne($sql, [$companyCd]);
+
+        if (!$row) {
+            return null;
+        }
+
+        $row->parts = $row->parts ? json_decode($row->parts) : [];
+
+        $filmos = DB::select("
+            SELECT
+                movie_cd AS movieCd,
+                movie_nm AS movieNm,
+                company_part_nm AS companyPartNm
+            FROM tb_company_filmo
+            WHERE company_cd = ?
+            ORDER BY id ASC;
+        ", [$companyCd]);
+
+        $row->filmos = $filmos;
+
+        return $row;
+    }
+
+    /**
+     * 영화사 존재 여부 (상세 정보 수집 완료 기준: parts NOT NULL)
+     */
+    public function isExists(string $companyCd): int
+    {
+        $sql = "SELECT IF(COUNT(*) > 0, 1, 0) AS isExists FROM tb_company WHERE company_cd = ? AND parts IS NOT NULL;";
+
+        return DB::selectOne($sql, [$companyCd])->isExists;
+    }
+
+    private function buildWhereQuery(SearchData $params): string
+    {
+        $where = "";
+
+        if ($params->keyword) {
+            $where .= sprintf(" AND (CMP.company_nm LIKE '%%%s%%' OR CMP.company_nm_en LIKE '%%%s%%') ", $params->keyword, $params->keyword);
+        }
+
+        if ($params->ceo) {
+            $where .= sprintf(" AND CMP.ceo_nm LIKE '%%%s%%' ", $params->ceo);
+        }
+
+        return $where;
+    }
+}