diff --git a/app/utils/nvMapScraper.py b/app/utils/nvMapScraper.py index 5538139..a029e0d 100644 --- a/app/utils/nvMapScraper.py +++ b/app/utils/nvMapScraper.py @@ -37,10 +37,11 @@ class NvMapScraper: data_source_identifier = "nv" SUPPLEMENT_THRESHOLD = 30 # 업체 사진이 이 수 미만일 때 방문자 사진으로 보충 # 방문자 사진 보충 시의 합산 상한 (필터링·정책상 제한). - # 업체 제공 사진은 필터링 면제 대상이므로 이 상한과 무관하게 전량 사용한다. + # 업체 제공 사진은 필터링 면제 대상이므로 이 상한과 무관하게 수집분을 전부 사용한다 + # (수집 자체는 BIZ_MAX_PAGES가 상한 — 초과 시 warning 로그 발생). MAX_IMAGES = 50 BIZ_PAGE_SIZE = 20 # getPhotoViewerItems 'biz' 커서의 페이지당 사진 수 - BIZ_MAX_PAGES = 5 # 업체 사진 페이지네이션 안전 상한 (5×20=100장) + BIZ_MAX_PAGES = 5 # 업체 사진 수집 상한 (5×20=100장, 도달 시 warning) OVERVIEW_QUERY: str = """ query getAccommodation($id: String!, $deviceType: String) { business: placeDetail(input: {id: $id, isNx: true, deviceType: $deviceType}) { @@ -282,7 +283,7 @@ query getVisitorReviewStats($id: String!) { # (필터링 없는 기본 조립. 마케팅 적합성 필터를 적용하려면 호출측에서 # owner_images / extra_photo_urls를 직접 사용해 image_filter.assemble_images로 재조립할 것.) # MAX_IMAGES 상한은 방문자 사진이 섞이는 보충 경로에만 적용하며(필터링·정책상 제한), - # 업체 제공 사진만으로 구성되는 경우는 전량 사용한다. + # 업체 제공 사진만으로 구성되는 경우는 수집분(최대 BIZ_MAX_PAGES 페이지)을 전부 사용한다. if len(self.owner_images) < self.SUPPLEMENT_THRESHOLD: combined = self._dedup_by_original(self.owner_images + self.extra_photo_urls) logger.info( @@ -366,16 +367,16 @@ query getVisitorReviewStats($id: String!) { }, "query": self.PHOTO_VIEWER_QUERY, } - # 업체 등록 사진 전체. placeDetail.images는 대표 사진 일부(1~수 장)만 반환하는 - # 경우가 있어, 사진 탭이 실제 사용하는 'biz' 커서로 전량을 별도 조회해 보강한다. + # 업체 등록 사진. placeDetail.images는 대표 사진 일부(1~수 장)만 반환하는 + # 경우가 있어, 사진 탭이 실제 사용하는 'biz' 커서로 별도 조회해 보강한다. # biz 커서는 페이지당 BIZ_PAGE_SIZE(20)장이며 lastCursor가 단순 인덱스 문자열 - # ("20", "40")이라 이전 응답 없이도 다음 페이지를 미리 요청할 수 있고, 범위를 + # ("20", "40")이라 이전 응답 없이도 모든 페이지를 미리 만들 수 있고, 범위를 # 벗어난 페이지는 빈 목록을 반환하므로 블라인드 요청해도 안전하다. - # 첫 배치에 3페이지를 실어 보내고, 마지막 페이지가 가득 차 있으면(=더 있을 수 - # 있음) 추가 배치를 반복 요청해 업체 사진을 전량 수집한다. - BIZ_INITIAL_PAGES = 3 + # 따라서 상한(BIZ_MAX_PAGES)까지의 전 페이지를 첫 배치에 한꺼번에 실어 보낸다 + # — 추가 왕복(페이지 재탐색 + WTM 토큰 재캡처)이 없고, 개별 페이지 실패가 + # 이후 페이지 수집을 막지 못한다. biz_payloads = [ - self._build_biz_payload(place_id, page) for page in range(BIZ_INITIAL_PAGES) + self._build_biz_payload(place_id, page) for page in range(self.BIZ_MAX_PAGES) ] payloads = [overview_payload, stats_payload, interior_payload, exterior_payload, review_payload, *biz_payloads] @@ -416,30 +417,21 @@ query getVisitorReviewStats($id: String!) { exterior_urls = self._extract_photo_viewer_urls(results[3] if len(results) > 3 else None) review_urls = self._extract_photo_viewer_urls(results[4] if len(results) > 4 else None) - biz_pages = list(results[5:]) + biz_pages = results[5:] biz_urls = [url for page_result in biz_pages for url in self._extract_photo_viewer_urls(page_result)] - # 마지막 페이지가 가득 차 있으면 다음 배치를 계속 요청해 업체 사진을 전량 수집한다 - # (업체 사진은 필터링 면제 대상이라 많을수록 영상 소재 다양성이 좋아짐). - # 후속 배치 실패는 warning만 남기고 이미 수집한 분량으로 진행한다 (best-effort). - next_page = BIZ_INITIAL_PAGES - while ( - biz_pages - and self._raw_photo_count(biz_pages[-1]) >= self.BIZ_PAGE_SIZE - and next_page < self.BIZ_MAX_PAGES - ): - chunk_pages = range(next_page, min(next_page + 3, self.BIZ_MAX_PAGES)) - chunk_payloads = [self._build_biz_payload(place_id, p) for p in chunk_pages] - try: - chunk_results = await NvMapPwScraper.fetch_graphql(place_id, chunk_payloads) - except Exception as e: - logger.warning(f"[NvMapScraper] 업체 사진 추가 페이지 수집 실패(수집분으로 진행): {e}") - break - if not chunk_results: - logger.warning("[NvMapScraper] 업체 사진 추가 페이지 응답 없음(수집분으로 진행)") - break - biz_pages = [r for r in chunk_results if r is not None] - biz_urls += [url for page_result in biz_pages for url in self._extract_photo_viewer_urls(page_result)] - next_page += len(chunk_payloads) + # 개별 페이지 실패(None)는 해당 20장만 누락되고 나머지 페이지는 영향 없다 (best-effort). + failed_biz_pages = sum(1 for r in biz_pages if r is None) + if failed_biz_pages: + logger.warning( + f"[NvMapScraper] 업체 사진 {failed_biz_pages}개 페이지 응답 실패 " + f"— 페이지당 최대 {self.BIZ_PAGE_SIZE}장 누락 가능 (수집분으로 진행)" + ) + # 마지막 페이지까지 가득 차 있으면 상한 밖에 사진이 더 있을 수 있다. + if biz_pages and self._raw_photo_count(biz_pages[-1]) >= self.BIZ_PAGE_SIZE: + logger.warning( + f"[NvMapScraper] 업체 사진이 수집 상한(BIZ_MAX_PAGES={self.BIZ_MAX_PAGES}, " + f"{self.BIZ_MAX_PAGES * self.BIZ_PAGE_SIZE}장)까지 가득 참 — 초과분은 수집되지 않음" + ) extra_photo_urls = self._interleave(interior_urls, exterior_urls) + review_urls logger.info(