movie 사이트(Laravel)와 crawler(Go/gin)가 KOBIS OpenAPI를 공유하는 구조에서, Laravel이 crawler에 전달하는 KOBIS 인증키 3개가 전부 무효화되어 박스오피스 신규 데이터 수집이 중단된 상태다(일별: 2026-03-12, 주간: 2026-06-07 이후 중단 — 로컬 DB 기준). crawler는 KOBIS 오류 응답(faultInfo)을 감지하지 못해 조용히 빈 목록을 반환하고, Laravel은 crawler 장애 시 500 에러가 난다. 본 계획은 키 동기화, 오류 감지/격리, 뷰 방어 처리, 수집 재가동을 다룬다.
As a movie 사이트 방문자, I want /movie/rank 에서 최신 일별·주간 박스오피스를 정상 조회하고 crawler 장애 시에도 안내를 받기를, So that 데이터 공백이나 500 에러 없이 서비스를 이용할 수 있다.
320010) → crawler가 그대로 KOBIS에 전달 → faultInfo 응답 → crawler는 200 + 빈 목록 반환 → 사이트는 원인 표시 없이 빈 페이지. crawler 다운 시 Http::get ConnectionException 미처리로 500.| 항목 | 결과 |
|---|---|
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 컬럼) |
searchCompanyList/searchCompanyInfo), 영화인, 공통코드 API가 정리되어 있으나 crawler는 이 중 어느 것도 구현하지 않음 (구현: 일별/주간 박스오피스, 영화 목록, 영화 상세 4종 + 상세페이지 스크래핑).info->Companys JSON으로만 노출됨 (show.blade.php:270-286)./movie/rank?date=2026-07-21
┌────────────────────────────────┐
│ 인기 순위 │
│ (빈 화면 — 무효 키로 인해 │
│ 목록 없음, 원인 표시 없음) │
│ crawler 다운 시: 500 에러 페이지 │
└────────────────────────────────┘
/movie/rank?date=2026-07-21
┌────────────────────────────────┐
│ 인기 순위 │
│ [1위 포스터][2위 포스터]... │ ← 유효 키로 on-demand 수집 재개
│ crawler 장애 시: │
│ "일시적으로 순위 정보를 불러올 │
│ 수 없습니다" 안내 (200 응답) │
└────────────────────────────────┘
| Touchpoint | Before | After | Notes |
|---|---|---|---|
| /movie/rank (미수집 날짜) | 빈 목록 | 데이터 표시 | 키 교체로 해결 |
| /movie/rank (crawler 다운) | 500 | 안내 문구 | timeout+catch |
| /movie/search/{id} 누적 매출/관객 | 0원/0명 표시 |
값 없으면 행 숨김 | sale_acc 대부분 NULL |
| /movie/rank/{id} 등 상세 | 동일 | 동일 | 변경 없음 |
| 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 패턴 |
| 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으로 반환됨 |
// 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
}
// 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")
// 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 으로 보정할 것
}
// 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";
}
{{-- 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
// 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"`
...
}
| 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 | 구현 현황 매트릭스 + 키 운영 정책 추가 |
searchCompanyList/searchCompanyInfo 프록시 — 신규 기능, 별도 PRD로getKFCC0502, 주석 처리된 /cron/stats) 부활0=주간 유지 — 주말 진입 시 주간 순위를 보여주는 현행 동작은 의도로 간주)movie/config/constants.php:300-302 KOBIS_API_1/2/3 값 교체. crawler/config/movie.json apiKey_3 교체.const KOBIS_API_1 = '...';)key 파라미터를 그대로 KOBIS에 전달하므로 Laravel 상수 교체가 실질적 수정점. crawler의 movie.json은 cron 수집 전용. 배포 서버의 movie.json은 별도 반영 필요(git 관리 여부 확인).curl "http://www.kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json?key=<새 KOBIS_API_1>&targetDt=<어제>" → boxOfficeResult 반환 확인.crawler/controller/cron.go:492-500의 하드코딩 무효 키 제거.IMPLEMENT:
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 ./... 통과.
crawler/model/kobis.go의 4개 API 함수(MovieDailyBoxOfficeListAPI, MovieWeeklyBoxOfficeListAPI, MovieListAPI, MovieInfoAPI)에서 KOBIS 오류 응답을 error로 승격.IMPLEMENT: 파일 상단에 공용 타입/헬퍼 추가 후 각 함수의 json.Unmarshal 직전에 호출:
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+오류 메시지 반환.
crawler/controller/movie.go에서 KOBIS API 오류 시 c.JSON(http.StatusBadRequest, ...) 후 실행이 계속되는 3곳 수정.return 추가.c.JSON(...); return)go vet ./... + 무효 키 호출 시 단일 400 응답.Http::get에 타임아웃과 예외 처리 추가, abort 코드 보정, 뷰 null 방어.Http::timeout(5)->get($url, $params->toArray()) 로 교체, index()는 try { ... } catch (ConnectionException) { $data = null; }abort($e->getCode() ?: 500, $e->getMessage());desktop/movie/rank/index.blade.php:30 및 mobile 동일 파일: @if($data && $data->rows > 0) + @else 분기에 "일시적으로 순위 정보를 불러올 수 없습니다." 안내 (기존 빈 목록 안내 마크업이 있으면 재사용)use Illuminate\Http\Client\ConnectionException;total(테이블 전체 COUNT)이 0보다 클 수 있음 — 목록 유무 판단은 rows/list 기준으로.desktop/movie/search/show.blade.php 및 mobile 동일 파일의 누적 매출액/관객수 <tr>을 @if($detail->SaleAcc) / @if($detail->AudiAcc)로 감싸기. number_format(null) 경고 제거를 위해 ?? 0을 인자 안쪽으로(number_format($detail->SaleAcc ?? 0)).@if($detail->Poster) 조건부 섹션 패턴searchMovieDtl.do sType=stat) 주석 처리. 재활성화는 셀렉터 재검증이 필요한 별도 작업 — cron.go:324-335, movie.go:524-537 주석 참조. 후속 과제로 남김.SearchController::index(Request $request, ListParams $params)의 $params가 84행에서 SearchData::fromRequest로 즉시 덮어써짐 — 시그니처에서 ListParams 제거. 미사용 상수 MOVIE_LIST(constants.php:306)에 // unused: 검색 페이지는 DB 직조회 주석 또는 제거.use App\Models\DTO\Movie\ListParams; 임포트도 함께 정리. MOVIE_LIST 상수는 crawler 라우트에는 존재하므로 문서화 후 유지해도 무방.php -l 통과, /movie/search 정상 렌더.192.168.0.10:1050, prod 192.168.0.100:1050) 기동 확인 후, 스케줄 트리거를 문서화. 두 서버 모두 현재 이 개발 PC에서 /ping 무응답이었음.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에 저장됨)curl http://<host>:1050/ping → pong, curl http://<host>:1050/db → ms 응답.| 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 렌더 |
Detail이 null인 daily row ($row->Detail->MainImg — rank/index:36, null 가드 확인)# crawler (Go)
cd E:\server\project\crawler; go build ./...; go vet ./...
EXPECT: 빌드/벳 무오류
# 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
# 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
-- 수집 재개 확인 (rank 페이지 1회 조회 후)
SELECT MAX(show_range) FROM movie.tb_movie_daily; -- 어제 날짜 포함
SELECT MAX(created_at) FROM movie.tb_movie_daily; -- 오늘 타임스탬프
go build/go vet/php -l 무오류| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| 유효 키 2개도 소유자 불명/추후 만료 | 중 | 높음 | KOBIS 계정으로 신규 발급, 발급 주체 문서화 |
| crawler 서버 접근 불가(현재 dev/prod 무응답) | 중 | 높음 | 서버 기동 상태 우선 확인, 로컬 기동으로 검증 대체 |
| 배포 서버의 movie.json이 git 밖 관리 | 중 | 중 | 배포 절차에 config 반영 단계 명시 |
| KOBIS 일 호출 쿼터 초과 | 낮 | 중 | cron 수집 시 key 파라미터 분산(기존 GetKey 설계 유지) |
| daily/weekly 과거 공백(3~7월) 미백필 | 확실 | 낮 | on-demand 구조라 사용자 조회 시 자동 채움 — 별도 조치 불필요 |
$row->MovieCd, $detail->MainImg). crawler 구조체에 json 태그를 추가하면 사이트가 즉시 깨지므로 금지.SELECT * positional scan ↔ DB 컬럼 순서 의존 — 스키마 변경 시 양쪽 동시 수정 필요 (현재는 정합)./prp-prd)로 진행..env/.env.production의 미커밋 변경(크리덴셜 → 환경변수 치환)은 본 건과 무관한 진행 중 작업으로 보임 — 건드리지 않음.