|
|
@@ -0,0 +1,413 @@
|
|
|
+# Plan: KOBIS Crawler ↔ Movie 사이트 동기화 오류 수정
|
|
|
+
|
|
|
+## Summary
|
|
|
+movie 사이트(Laravel)와 crawler(Go/gin)가 KOBIS OpenAPI를 공유하는 구조에서, **Laravel이 crawler에 전달하는 KOBIS 인증키 3개가 전부 무효화**되어 박스오피스 신규 데이터 수집이 중단된 상태다(일별: 2026-03-12, 주간: 2026-06-07 이후 중단 — 로컬 DB 기준). crawler는 KOBIS 오류 응답(`faultInfo`)을 감지하지 못해 조용히 빈 목록을 반환하고, Laravel은 crawler 장애 시 500 에러가 난다. 본 계획은 키 동기화, 오류 감지/격리, 뷰 방어 처리, 수집 재가동을 다룬다.
|
|
|
+
|
|
|
+## User Story
|
|
|
+As a movie 사이트 방문자,
|
|
|
+I want /movie/rank 에서 최신 일별·주간 박스오피스를 정상 조회하고 crawler 장애 시에도 안내를 받기를,
|
|
|
+So that 데이터 공백이나 500 에러 없이 서비스를 이용할 수 있다.
|
|
|
+
|
|
|
+## Problem → Solution
|
|
|
+- **현재**: Laravel 상수의 KOBIS 키 3개 무효(라이브 검증: errorCode `320010`) → crawler가 그대로 KOBIS에 전달 → `faultInfo` 응답 → crawler는 200 + 빈 목록 반환 → 사이트는 원인 표시 없이 빈 페이지. crawler 다운 시 `Http::get` ConnectionException 미처리로 500.
|
|
|
+- **해결**: 유효 키로 동기화(양쪽), crawler에 faultInfo 감지 추가, Laravel에 타임아웃+예외 격리, 뷰 null 방어, cron 수집 재가동.
|
|
|
+
|
|
|
+## Metadata
|
|
|
+- **Complexity**: Medium (2개 리포지토리, ~12 files, 신규 개념 없음)
|
|
|
+- **Source PRD**: N/A (free-form)
|
|
|
+- **PRD Phase**: N/A
|
|
|
+- **Estimated Files**: movie 7 + crawler 4 + docs 1
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 검증된 사실 (2026-07-22 라이브 테스트)
|
|
|
+
|
|
|
+| 항목 | 결과 |
|
|
|
+|---|---|
|
|
|
+| Laravel `KOBIS_API_1` `d470c8a1...` | ❌ 무효 (`320010 유효하지않은 키값입니다`) |
|
|
|
+| Laravel `KOBIS_API_2` `52c8406a...` | ❌ 무효 |
|
|
|
+| Laravel `KOBIS_API_3` `f5eef342...` | ❌ 무효 (crawler `cron.go GetKey()` 기본 fallback으로 하드코딩되어 있음) |
|
|
|
+| crawler `movie.json apiKey_1` `b453d855...` | ✅ **유효** (박스오피스 데이터 정상 반환) |
|
|
|
+| crawler `movie.json apiKey_2` `cf7ea46f...` | ✅ **유효** |
|
|
|
+| crawler `movie.json apiKey_3` `82ca741a...` | ❌ 무효 — KOBIS 공식 문서의 **샘플 키**가 그대로 들어있음 |
|
|
|
+| KOBIS REST 응답 구조 | 문서(`E:\server\project\crawler\docs\KOBIS-영화API.md`)와 일치 — **API 자체는 변경 없음** |
|
|
|
+| KOBIS 상세페이지 스크래핑 셀렉터 (`item_tab basic`, `desc_info`, `fl thumb`, `#post`, `#stl`) | ✅ 여전히 유효 |
|
|
|
+| TMDB 키 (`bfeb1c9a...`) | ✅ 유효 (메인/트레일러 페이지 정상) |
|
|
|
+| crawler 서버 dev `192.168.0.10:1050` / prod `192.168.0.100:1050` | ⚠️ 현 개발 PC에서 응답 없음 (구동 여부 확인 필요) |
|
|
|
+| DB 데이터 신선도 | tb_movie_daily 최종 2026-03-12, tb_movie_weekly 2026-06-07, tb_movie/tb_movie_info 2026-03-12 |
|
|
|
+| tb_movie_detail `sale_acc`/`audi_acc` | 80,617행 중 ~15,000행만 채워짐 — crawler의 통계 스크래퍼(c2)가 주석 처리된 상태 |
|
|
|
+| DB 스키마 ↔ crawler positional `SELECT *` 스캔 | ✅ 컬럼 순서 정확히 일치 (tb_movie 16, tb_movie_daily 23, tb_movie_weekly 24, tb_movie_info 20, tb_movie_detail 11 컬럼) |
|
|
|
+
|
|
|
+### 영화사(Company) 페이지 확인 결과
|
|
|
+- KOBIS 문서에는 영화사 목록/상세(`searchCompanyList`/`searchCompanyInfo`), 영화인, 공통코드 API가 정리되어 있으나 **crawler는 이 중 어느 것도 구현하지 않음** (구현: 일별/주간 박스오피스, 영화 목록, 영화 상세 4종 + 상세페이지 스크래핑).
|
|
|
+- movie 사이트에도 **영화사 전용 라우트/페이지 없음**. 회사 정보는 영화 상세 뷰의 "제작/수입/배급사" 섹션에서 `info->Companys` JSON으로만 노출됨 ([show.blade.php:270-286](resources/views/desktop/movie/search/show.blade.php)).
|
|
|
+- → 신규 기능이므로 본 계획의 범위에서 제외(NOT Building 참조).
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## UX Design
|
|
|
+
|
|
|
+### Before
|
|
|
+```
|
|
|
+/movie/rank?date=2026-07-21
|
|
|
+┌────────────────────────────────┐
|
|
|
+│ 인기 순위 │
|
|
|
+│ (빈 화면 — 무효 키로 인해 │
|
|
|
+│ 목록 없음, 원인 표시 없음) │
|
|
|
+│ crawler 다운 시: 500 에러 페이지 │
|
|
|
+└────────────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+### After
|
|
|
+```
|
|
|
+/movie/rank?date=2026-07-21
|
|
|
+┌────────────────────────────────┐
|
|
|
+│ 인기 순위 │
|
|
|
+│ [1위 포스터][2위 포스터]... │ ← 유효 키로 on-demand 수집 재개
|
|
|
+│ crawler 장애 시: │
|
|
|
+│ "일시적으로 순위 정보를 불러올 │
|
|
|
+│ 수 없습니다" 안내 (200 응답) │
|
|
|
+└────────────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+### Interaction Changes
|
|
|
+| Touchpoint | Before | After | Notes |
|
|
|
+|---|---|---|---|
|
|
|
+| /movie/rank (미수집 날짜) | 빈 목록 | 데이터 표시 | 키 교체로 해결 |
|
|
|
+| /movie/rank (crawler 다운) | 500 | 안내 문구 | timeout+catch |
|
|
|
+| /movie/search/{id} 누적 매출/관객 | `0원`/`0명` 표시 | 값 없으면 행 숨김 | sale_acc 대부분 NULL |
|
|
|
+| /movie/rank/{id} 등 상세 | 동일 | 동일 | 변경 없음 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Mandatory Reading
|
|
|
+
|
|
|
+| Priority | File | Lines | Why |
|
|
|
+|---|---|---|---|
|
|
|
+| P0 | `E:\server\project\movie\config\constants.php` | 297-309 | 무효 키 3개 + crawler 엔드포인트 상수 |
|
|
|
+| P0 | `E:\server\project\crawler\config\movie.json` | all | 유효 키 1,2 / 무효 샘플 키 3 |
|
|
|
+| P0 | `E:\server\project\crawler\controller\cron.go` | 492-500 | GetKey(): 무효 키 하드코딩 fallback |
|
|
|
+| P0 | `E:\server\project\crawler\model\kobis.go` | 14-36 | KOBIS 호출 + faultInfo 미검사 지점 |
|
|
|
+| P0 | `E:\server\project\crawler\controller\movie.go` | 73-77, 171-176, 251-254 | `c.JSON(400)` 후 `return` 누락 3곳 |
|
|
|
+| P0 | `E:\server\project\movie\app\Http\Controllers\Movie\RankController.php` | 90-119 | Http::get 미보호 + `$data->total` 처리 |
|
|
|
+| P1 | `E:\server\project\movie\resources\views\desktop\movie\rank\index.blade.php` | 30 | `$data->rows` null 미방어 (mobile 동일 파일 존재) |
|
|
|
+| P1 | `E:\server\project\movie\resources\views\desktop\movie\search\show.blade.php` | 107-126 | SaleAcc/AudiAcc 0 표시 문제 (mobile 동일) |
|
|
|
+| P1 | `E:\server\project\movie\app\Models\DTO\Movie\BoxOfficeParams.php` | 10-21 | key 주입 지점 (`KOBIS_API_1`) |
|
|
|
+| P2 | `E:\server\project\crawler\model\movieDaily.go` | 21-58 | 요청/응답 구조체 패턴 |
|
|
|
+| P2 | `E:\server\project\movie\app\Http\Controllers\Movie\SearchController.php` | 43, 117-169 | ListParams 죽은 주입 + show()의 try/catch 패턴 |
|
|
|
+
|
|
|
+## External Documentation
|
|
|
+
|
|
|
+| Topic | Source | Key Takeaway |
|
|
|
+|---|---|---|
|
|
|
+| KOBIS OpenAPI 명세 | `E:\server\project\crawler\docs\KOBIS-영화API.md` | 9종 API. 응답 구조 현행과 일치. 키는 발급 필요 |
|
|
|
+| KOBIS 키 발급 | https://www.kobis.or.kr/kobisopenapi (키 발급/관리 메뉴) | 무효 키 3개 대체분 발급 필요 시 사용자 계정 필요 |
|
|
|
+| 오류 응답 형식 | 라이브 검증 | `{"faultInfo":{"message":"...","errorCode":"320010"}}` — HTTP 200으로 반환됨 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Patterns to Mirror
|
|
|
+
|
|
|
+### GO_ERROR_HANDLING (crawler)
|
|
|
+```go
|
|
|
+// SOURCE: E:\server\project\crawler\model\kobis.go:25-33
|
|
|
+data, err := this.Rest.CallRestGetAPI(url)
|
|
|
+if this.Rest.Check(err) { // Check()가 error.txt 로그 기록 후 bool 반환
|
|
|
+ return result, err
|
|
|
+}
|
|
|
+
|
|
|
+err = json.Unmarshal(data, &result)
|
|
|
+if this.Rest.Check(err) {
|
|
|
+ return result, err
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### GO_LOGGING (crawler DB 로그)
|
|
|
+```go
|
|
|
+// SOURCE: E:\server\project\crawler\model\movieDaily.go:96-103
|
|
|
+err := conn.QueryRow(query).Scan(&total)
|
|
|
+if err != nil {
|
|
|
+ db.SetErrorLog(err, query)
|
|
|
+ return total
|
|
|
+}
|
|
|
+db.SetGeneralLog(config.GL_ACTION_SELECT, query, "select total movie daily")
|
|
|
+```
|
|
|
+
|
|
|
+### PHP_CONTROLLER_ERROR_HANDLING (movie)
|
|
|
+```php
|
|
|
+// SOURCE: E:\server\project\movie\app\Http\Controllers\Movie\RankController.php:129-186
|
|
|
+try {
|
|
|
+ ...
|
|
|
+ $response = Http::get($url, $params->toArray());
|
|
|
+ if(!$response->ok()) {
|
|
|
+ throw new Exception('요청한 정보가 없습니다.');
|
|
|
+ }
|
|
|
+ ...
|
|
|
+}catch(Exception $e) {
|
|
|
+ abort($e->getCode(), $e->getMessage()); // 주의: getCode()=0 → invalid status. ?: 500 으로 보정할 것
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### PHP_DTO_PARAMS (movie)
|
|
|
+```php
|
|
|
+// SOURCE: E:\server\project\movie\app\Models\DTO\Movie\BoxOfficeParams.php:10-21
|
|
|
+public function __construct(
|
|
|
+ public string $targetDt = "",
|
|
|
+ ...
|
|
|
+) {
|
|
|
+ $this->key = KOBIS_API_1; // ← 키 주입 지점. 상수 교체만으로 반영됨
|
|
|
+ $this->weekGb = "0";
|
|
|
+ $this->itemPerPage = "10";
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### BLADE_JSON_FIELD (movie 뷰 — crawler Go 구조체는 JSON 태그가 없어 PascalCase로 직렬화됨)
|
|
|
+```blade
|
|
|
+{{-- SOURCE: E:\server\project\movie\resources\views\desktop\movie\rank\show.blade.php:50-58 --}}
|
|
|
+@if($info->Directors)
|
|
|
+ @foreach(json_decode($info->Directors) as $director)
|
|
|
+ {{ $director->peopleNm }}
|
|
|
+ @endforeach
|
|
|
+@endif
|
|
|
+```
|
|
|
+
|
|
|
+### GO_GIN_BINDING (crawler 요청 파라미터)
|
|
|
+```go
|
|
|
+// SOURCE: E:\server\project\crawler\model\movieDaily.go:21-28
|
|
|
+type SearchDailyBoxOfficeParams struct {
|
|
|
+ Key string `form:"key" url:"key" binding:"required"`
|
|
|
+ TargetDt string `form:"targetDt" url:"targetDt" binding:"required"`
|
|
|
+ ...
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Files to Change
|
|
|
+
|
|
|
+| File | Action | Justification |
|
|
|
+|---|---|---|
|
|
|
+| `movie/config/constants.php` | UPDATE | KOBIS_API_1/2/3 → 유효 키로 교체 |
|
|
|
+| `crawler/config/movie.json` | UPDATE | apiKey_3 샘플 키 → 유효 키로 교체 |
|
|
|
+| `crawler/controller/cron.go` | UPDATE | GetKey() fallback을 config 참조로, key=3 지원 |
|
|
|
+| `crawler/model/kobis.go` | UPDATE | faultInfo 감지 → error 반환 (4개 API 함수) |
|
|
|
+| `crawler/controller/movie.go` | UPDATE | c.JSON(400) 후 return 누락 3곳 수정 |
|
|
|
+| `movie/app/Http/Controllers/Movie/RankController.php` | UPDATE | Http timeout+catch, abort 코드 보정 |
|
|
|
+| `movie/app/Http/Controllers/Movie/SearchController.php` | UPDATE | show() Http timeout, 죽은 ListParams 주입 제거 |
|
|
|
+| `movie/resources/views/desktop/movie/rank/index.blade.php` | UPDATE | `$data` null 방어 + 장애 안내 |
|
|
|
+| `movie/resources/views/mobile/movie/rank/index.blade.php` | UPDATE | 동일 |
|
|
|
+| `movie/resources/views/desktop/movie/search/show.blade.php` | UPDATE | SaleAcc/AudiAcc null 시 행 숨김 |
|
|
|
+| `movie/resources/views/mobile/movie/search/show.blade.php` | UPDATE | 동일 |
|
|
|
+| `crawler/docs/KOBIS-영화API.md` | UPDATE | 구현 현황 매트릭스 + 키 운영 정책 추가 |
|
|
|
+
|
|
|
+## NOT Building
|
|
|
+- 영화사(company) 목록/상세 페이지 및 crawler의 `searchCompanyList`/`searchCompanyInfo` 프록시 — 신규 기능, 별도 PRD로
|
|
|
+- 영화인(people)/공통코드(code) API 연동
|
|
|
+- KCISA 박스오피스 통계(`getKFCC0502`, 주석 처리된 `/cron/stats`) 부활
|
|
|
+- tb_movie_* 스키마 변경 (현재 정합함)
|
|
|
+- weekGb 정책 변경 (현행 `0`=주간 유지 — 주말 진입 시 주간 순위를 보여주는 현행 동작은 의도로 간주)
|
|
|
+- crawler c2(누적 매출/관객 스크래퍼) 재활성화 — 후속 과제로 기록만 (Task 6 GOTCHA 참조)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Step-by-Step Tasks
|
|
|
+
|
|
|
+### Task 1: KOBIS 키 동기화 (양쪽 리포)
|
|
|
+- **ACTION**: 유효 키로 양쪽 통일. 검증된 유효 키: crawler movie.json의 apiKey_1, apiKey_2. 3번째 키는 KOBIS에서 신규 발급(사용자 계정 필요) 전까지 key_1 재사용.
|
|
|
+- **IMPLEMENT**: `movie/config/constants.php:300-302` KOBIS_API_1/2/3 값 교체. `crawler/config/movie.json` apiKey_3 교체.
|
|
|
+- **MIRROR**: 기존 상수 선언 형식 유지 (`const KOBIS_API_1 = '...';`)
|
|
|
+- **IMPORTS**: 없음
|
|
|
+- **GOTCHA**: crawler는 Laravel이 넘긴 `key` 파라미터를 그대로 KOBIS에 전달하므로 **Laravel 상수 교체가 실질적 수정점**. crawler의 movie.json은 cron 수집 전용. 배포 서버의 movie.json은 별도 반영 필요(git 관리 여부 확인).
|
|
|
+- **VALIDATE**: `curl "http://www.kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json?key=<새 KOBIS_API_1>&targetDt=<어제>"` → `boxOfficeResult` 반환 확인.
|
|
|
+
|
|
|
+### Task 2: crawler GetKey() fallback 수정
|
|
|
+- **ACTION**: `crawler/controller/cron.go:492-500`의 하드코딩 무효 키 제거.
|
|
|
+- **IMPLEMENT**:
|
|
|
+```go
|
|
|
+func GetKey(c *gin.Context) string {
|
|
|
+ switch c.Query("key") {
|
|
|
+ case "1":
|
|
|
+ return config.Movie.Kobis.ApiKey_1
|
|
|
+ case "2":
|
|
|
+ return config.Movie.Kobis.ApiKey_2
|
|
|
+ default:
|
|
|
+ return config.Movie.Kobis.ApiKey_3
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+- **MIRROR**: 기존 config.Movie.Kobis 접근 패턴
|
|
|
+- **IMPORTS**: 기존 `crawler/config` 이미 임포트됨
|
|
|
+- **GOTCHA**: apiKey_3가 Task 1에서 유효 키로 교체된 후에만 의미 있음.
|
|
|
+- **VALIDATE**: `go build ./...` 통과.
|
|
|
+
|
|
|
+### Task 3: crawler faultInfo 감지
|
|
|
+- **ACTION**: `crawler/model/kobis.go`의 4개 API 함수(MovieDailyBoxOfficeListAPI, MovieWeeklyBoxOfficeListAPI, MovieListAPI, MovieInfoAPI)에서 KOBIS 오류 응답을 error로 승격.
|
|
|
+- **IMPLEMENT**: 파일 상단에 공용 타입/헬퍼 추가 후 각 함수의 `json.Unmarshal` 직전에 호출:
|
|
|
+```go
|
|
|
+type KobisFault struct {
|
|
|
+ FaultInfo *struct {
|
|
|
+ Message string `json:"message"`
|
|
|
+ ErrorCode string `json:"errorCode"`
|
|
|
+ } `json:"faultInfo"`
|
|
|
+}
|
|
|
+
|
|
|
+func (this *Kobis) checkFault(data []byte) error {
|
|
|
+ var fault KobisFault
|
|
|
+ if json.Unmarshal(data, &fault) == nil && fault.FaultInfo != nil {
|
|
|
+ err := fmt.Errorf("KOBIS fault %s: %s", fault.FaultInfo.ErrorCode, fault.FaultInfo.Message)
|
|
|
+ this.Rest.Check(err) // error.txt 기록
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+```
|
|
|
+- **MIRROR**: GO_ERROR_HANDLING 패턴 (`if this.Rest.Check(err) { return result, err }`)
|
|
|
+- **IMPORTS**: `fmt` 추가
|
|
|
+- **GOTCHA**: KOBIS는 오류도 HTTP 200으로 반환하므로 상태코드 검사로는 잡을 수 없음. 반드시 body 검사.
|
|
|
+- **VALIDATE**: 무효 키로 crawler 엔드포인트 호출 시 200+빈 목록 대신 400+오류 메시지 반환.
|
|
|
+
|
|
|
+### Task 4: crawler 컨트롤러 return 누락 수정
|
|
|
+- **ACTION**: `crawler/controller/movie.go`에서 KOBIS API 오류 시 `c.JSON(http.StatusBadRequest, ...)` 후 실행이 계속되는 3곳 수정.
|
|
|
+- **IMPLEMENT**: 75-77행(SearchDailyBoxOfficeList), 174-176행(SearchWeeklyBoxOfficeList), 252-254행(SearchMovieList)에 `return` 추가.
|
|
|
+- **MIRROR**: 같은 파일 내 다른 오류 분기들(`c.JSON(...); return`)
|
|
|
+- **GOTCHA**: return 누락 시 gin이 이중 응답 기록을 시도하고, 오류인데도 빈 데이터로 후속 처리 진행됨.
|
|
|
+- **VALIDATE**: `go vet ./...` + 무효 키 호출 시 단일 400 응답.
|
|
|
+
|
|
|
+### Task 5: Laravel crawler 장애 격리
|
|
|
+- **ACTION**: RankController(index/show), SearchController(show)의 `Http::get`에 타임아웃과 예외 처리 추가, abort 코드 보정, 뷰 null 방어.
|
|
|
+- **IMPLEMENT**:
|
|
|
+ - `Http::timeout(5)->get($url, $params->toArray())` 로 교체, index()는 `try { ... } catch (ConnectionException) { $data = null; }`
|
|
|
+ - show()들의 catch 블록: `abort($e->getCode() ?: 500, $e->getMessage());`
|
|
|
+ - `desktop/movie/rank/index.blade.php:30` 및 mobile 동일 파일: `@if($data && $data->rows > 0)` + `@else` 분기에 "일시적으로 순위 정보를 불러올 수 없습니다." 안내 (기존 빈 목록 안내 마크업이 있으면 재사용)
|
|
|
+- **MIRROR**: PHP_CONTROLLER_ERROR_HANDLING 패턴 (SearchController::show의 try/catch 구조)
|
|
|
+- **IMPORTS**: `use Illuminate\Http\Client\ConnectionException;`
|
|
|
+- **GOTCHA**: crawler 응답은 rows=0이어도 `total`(테이블 전체 COUNT)이 0보다 클 수 있음 — 목록 유무 판단은 `rows`/`list` 기준으로.
|
|
|
+- **VALIDATE**: crawler 미기동 상태에서 /movie/rank 접속 → 500이 아닌 안내 문구 페이지.
|
|
|
+
|
|
|
+### Task 6: 누적 매출/관객 표시 방어 (search/show)
|
|
|
+- **ACTION**: tb_movie_detail의 sale_acc/audi_acc가 NULL인 80% 케이스에서 "0원/0명" 대신 행 숨김.
|
|
|
+- **IMPLEMENT**: `desktop/movie/search/show.blade.php` 및 mobile 동일 파일의 누적 매출액/관객수 `<tr>`을 `@if($detail->SaleAcc)` / `@if($detail->AudiAcc)`로 감싸기. `number_format(null)` 경고 제거를 위해 `?? 0`을 인자 안쪽으로(`number_format($detail->SaleAcc ?? 0)`).
|
|
|
+- **MIRROR**: 같은 뷰의 `@if($detail->Poster)` 조건부 섹션 패턴
|
|
|
+- **GOTCHA**: 근본 원인은 crawler의 c2 통계 스크래퍼(`searchMovieDtl.do sType=stat`) 주석 처리. 재활성화는 셀렉터 재검증이 필요한 별도 작업 — `cron.go:324-335`, `movie.go:524-537` 주석 참조. 후속 과제로 남김.
|
|
|
+- **VALIDATE**: sale_acc NULL인 영화 상세 페이지에서 해당 행 미표시.
|
|
|
+
|
|
|
+### Task 7: SearchController 죽은 코드 정리
|
|
|
+- **ACTION**: `SearchController::index(Request $request, ListParams $params)`의 `$params`가 84행에서 `SearchData::fromRequest`로 즉시 덮어써짐 — 시그니처에서 ListParams 제거. 미사용 상수 `MOVIE_LIST`(constants.php:306)에 `// unused: 검색 페이지는 DB 직조회` 주석 또는 제거.
|
|
|
+- **MIRROR**: RankController 시그니처 스타일
|
|
|
+- **GOTCHA**: `use App\Models\DTO\Movie\ListParams;` 임포트도 함께 정리. MOVIE_LIST 상수는 crawler 라우트에는 존재하므로 문서화 후 유지해도 무방.
|
|
|
+- **VALIDATE**: `php -l` 통과, /movie/search 정상 렌더.
|
|
|
+
|
|
|
+### Task 8: cron 수집 재가동 + 운영 문서화
|
|
|
+- **ACTION**: crawler 서버(dev `192.168.0.10:1050`, prod `192.168.0.100:1050`) 기동 확인 후, 스케줄 트리거를 문서화. 두 서버 모두 현재 이 개발 PC에서 `/ping` 무응답이었음.
|
|
|
+- **IMPLEMENT**: `crawler/docs/KOBIS-영화API.md`(또는 신규 `docs/OPERATIONS.md`)에 다음을 기록:
|
|
|
+ - 일별 수집: `GET /cron/list?key=1` → `GET /cron/info?key=2` → `GET /cron/detail` (마지막 페이지는 `log/kobis/page.txt`에 저장됨)
|
|
|
+ - 박스오피스는 사이트 조회 시 on-demand 수집이므로 별도 백필 불필요 (키만 유효하면 날짜별 첫 조회 때 채워짐)
|
|
|
+ - 키 소유/발급 정책: 유효 키 목록과 관리 주체
|
|
|
+ - 구현 현황 매트릭스: 9종 중 4종 구현(daily/weekly/movieList/movieInfo), company/people/code 미구현
|
|
|
+- **VALIDATE**: crawler 기동 후 `curl http://<host>:1050/ping` → `pong`, `curl http://<host>:1050/db` → ms 응답.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Testing Strategy
|
|
|
+
|
|
|
+### Unit / Integration Tests
|
|
|
+
|
|
|
+| Test | Input | Expected Output | Edge Case? |
|
|
|
+|---|---|---|---|
|
|
|
+| crawler faultInfo 감지 | 무효 키로 `/movie/searchDailyBoxOfficeList` | HTTP 400 + `KOBIS fault 320010...` | ✅ |
|
|
|
+| crawler 정상 수집 | 유효 키 + 어제 날짜 | 200 + rows=10, DB upsert | |
|
|
|
+| Laravel rank (crawler down) | crawler 중지 후 /movie/rank | 200 + 안내 문구 | ✅ |
|
|
|
+| Laravel rank (정상) | 유효 키 + 오늘 | 순위 목록 렌더 | |
|
|
|
+| Laravel search/show (sale_acc NULL) | 해당 movieCd | 누적 행 미표시 | ✅ |
|
|
|
+| rank/show 상세 | dailyID base64 | stats/info/detail 렌더 | |
|
|
|
+
|
|
|
+### Edge Cases Checklist
|
|
|
+- [ ] KOBIS가 200 + faultInfo 반환 (무효 키/쿼터 초과)
|
|
|
+- [ ] crawler 무응답(타임아웃)
|
|
|
+- [ ] 조회 날짜에 상영작 없음 (미래 날짜)
|
|
|
+- [ ] `Detail`이 null인 daily row (`$row->Detail->MainImg` — rank/index:36, null 가드 확인)
|
|
|
+- [ ] 주말 날짜 조회 (weekly 경로, weekGb=0)
|
|
|
+- [ ] base64 변조된 rank/show id
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Validation Commands
|
|
|
+
|
|
|
+### Static Analysis
|
|
|
+```powershell
|
|
|
+# crawler (Go)
|
|
|
+cd E:\server\project\crawler; go build ./...; go vet ./...
|
|
|
+```
|
|
|
+EXPECT: 빌드/벳 무오류
|
|
|
+
|
|
|
+```powershell
|
|
|
+# movie (PHP) — 수정 파일 문법 검사
|
|
|
+cd E:\server\project\movie
|
|
|
+php -l app/Http/Controllers/Movie/RankController.php
|
|
|
+php -l app/Http/Controllers/Movie/SearchController.php
|
|
|
+php -l config/constants.php
|
|
|
+php artisan config:clear
|
|
|
+```
|
|
|
+EXPECT: No syntax errors
|
|
|
+
|
|
|
+### Live API Validation
|
|
|
+```powershell
|
|
|
+# 1. 새 키 유효성
|
|
|
+curl "http://www.kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json?key=<KOBIS_API_1>&targetDt=<어제 yyyymmdd>"
|
|
|
+# EXPECT: {"boxOfficeResult":{...}} (faultInfo 아님)
|
|
|
+
|
|
|
+# 2. crawler 경유 (crawler 기동 후)
|
|
|
+curl "http://localhost:1050/movie/searchDailyBoxOfficeList?key=<KOBIS_API_1>&targetDt=<어제>"
|
|
|
+# EXPECT: {"total":N,"rows":10,"list":[...]}
|
|
|
+
|
|
|
+# 3. 무효 키 → 오류 승격 확인
|
|
|
+curl "http://localhost:1050/movie/searchDailyBoxOfficeList?key=invalid&targetDt=<어제>"
|
|
|
+# EXPECT: HTTP 400 + KOBIS fault 320010
|
|
|
+```
|
|
|
+
|
|
|
+### Database Validation
|
|
|
+```sql
|
|
|
+-- 수집 재개 확인 (rank 페이지 1회 조회 후)
|
|
|
+SELECT MAX(show_range) FROM movie.tb_movie_daily; -- 어제 날짜 포함
|
|
|
+SELECT MAX(created_at) FROM movie.tb_movie_daily; -- 오늘 타임스탬프
|
|
|
+```
|
|
|
+
|
|
|
+### Browser Validation
|
|
|
+- [ ] /movie/rank — 오늘/어제 데이터 표시
|
|
|
+- [ ] /movie/rank?date=<주말 날짜> — weekly 경로 정상
|
|
|
+- [ ] crawler 중지 후 /movie/rank — 500 아닌 안내 문구
|
|
|
+- [ ] /movie/search → 상세 — 누적 매출/관객 NULL 행 숨김
|
|
|
+- [ ] /movie/rank/{id} 상세 — 감독/장르/배우/제작사 JSON 필드 렌더
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Acceptance Criteria
|
|
|
+- [ ] 양쪽 리포의 KOBIS 키가 동일한 유효 키 세트로 통일
|
|
|
+- [ ] crawler가 KOBIS faultInfo를 400으로 승격 + error.txt 기록
|
|
|
+- [ ] crawler 오류 분기 3곳 return 수정
|
|
|
+- [ ] Laravel rank 페이지가 crawler 장애 시 200 + 안내
|
|
|
+- [ ] tb_movie_daily에 최신 날짜 데이터 적재 확인
|
|
|
+- [ ] `go build`/`go vet`/`php -l` 무오류
|
|
|
+
|
|
|
+## Completion Checklist
|
|
|
+- [ ] 코드가 발견된 패턴(Rest.Check, SetErrorLog, try/catch+abort)을 따름
|
|
|
+- [ ] 하드코딩 키가 코드(cron.go)에 남아있지 않음
|
|
|
+- [ ] mobile 뷰도 desktop과 동일하게 수정됨
|
|
|
+- [ ] crawler docs에 구현 현황/키 정책 반영
|
|
|
+- [ ] 범위 외 작업(영화사 페이지 등) 미포함
|
|
|
+
|
|
|
+## Risks
|
|
|
+| Risk | Likelihood | Impact | Mitigation |
|
|
|
+|---|---|---|---|
|
|
|
+| 유효 키 2개도 소유자 불명/추후 만료 | 중 | 높음 | KOBIS 계정으로 신규 발급, 발급 주체 문서화 |
|
|
|
+| crawler 서버 접근 불가(현재 dev/prod 무응답) | 중 | 높음 | 서버 기동 상태 우선 확인, 로컬 기동으로 검증 대체 |
|
|
|
+| 배포 서버의 movie.json이 git 밖 관리 | 중 | 중 | 배포 절차에 config 반영 단계 명시 |
|
|
|
+| KOBIS 일 호출 쿼터 초과 | 낮 | 중 | cron 수집 시 key 파라미터 분산(기존 GetKey 설계 유지) |
|
|
|
+| daily/weekly 과거 공백(3~7월) 미백필 | 확실 | 낮 | on-demand 구조라 사용자 조회 시 자동 채움 — 별도 조치 불필요 |
|
|
|
+
|
|
|
+## Notes
|
|
|
+- **KOBIS API 자체는 변경되지 않음** — 문서와 응답 구조 일치(라이브 검증). "API가 많이 변경됐을 것"이라는 가설의 실체는 **키 무효화**였음.
|
|
|
+- crawler Go 구조체(MovieDailyTable 등)는 JSON 태그가 없어 PascalCase로 직렬화되며, Laravel 컨트롤러/뷰가 이에 의존(`$row->MovieCd`, `$detail->MainImg`). **crawler 구조체에 json 태그를 추가하면 사이트가 즉시 깨지므로 금지.**
|
|
|
+- crawler `SELECT *` positional scan ↔ DB 컬럼 순서 의존 — 스키마 변경 시 양쪽 동시 수정 필요 (현재는 정합).
|
|
|
+- 영화사/영화인/공통코드 API는 미구현 — 필요 시 별도 PRD(`/prp-prd`)로 진행.
|
|
|
+- `.env`/`.env.production`의 미커밋 변경(크리덴셜 → 환경변수 치환)은 본 건과 무관한 진행 중 작업으로 보임 — 건드리지 않음.
|