SearchBox 에 옛 row 가 노출되던 문제 — confidence ≥ 0.8 인덱스 가드 + 자동수집 95 키워드/2회/Geocoding 자동화 — POP-SPOT v2.13 (+v2.13.1~v2.13.3.1)
Algolia 인덱싱에 reviewStatus/status/confidence/endDate 추가 + SearchBox 클라 이중 가드. 자동수집 04:00+16:00 2회, 키워드 50→95, Geocoding 04:30 cron 자동화, 재인덱싱 API. v2.13.1~v2.13.3.1 핫픽스 포함.
메인 상단 SearchBox 에 "이게 왜 검색 결과에 뜨지?" 싶은 오래된 row 가 조금씩 떨어져 나오고 있었음. 자동수집의 정확도 임계값은 그대로 두면서 "그 이하는 인덱스에 안 올라갈 것" 을 강제.
이 글에서 다루는 것
모르는 단어 한 줄로
| 용어 | 한 줄 설명 |
|---|---|
| Algolia | 외부 전문 검색 서비스. "결과 미리 계산해서 빠르게 돌려주는" 구조. POP-SPOT 는 자체 검색과 함께 병행해서 쓰는 중 |
| confidence | 자동수집한 팝업이 "진짜일 확률" 을 0~1 사이 숫자로 표시한 값. 0.8 이상이면 자동 게시 |
| reviewStatus | AUTO_PUBLISHED / APPROVED / PENDING / REJECTED 같은 운영 검수 상태 |
| EXPIRED | 종료일이 오늘보다 과거인 팝업. 05:00 cron 이 매일 일괄 처리 |
| Geocoding | 주소를 좌표 (lat/lng) 으로 변환하는 일 |
무엇이 바뀌었나
| 항목 | v2.12 | v2.13 |
|---|---|---|
| 인덱스 속성 | name/location/category/content/imageUrl | • reviewStatus/status/confidence/endDate |
| 인덱스 가드 | 없음 — 모든 row 포함 | AUTO_PUBLISHED ∪ APPROVED ∪ null + status not EXPIRED/PENDING + confidence ≥ 0.8 강제 |
| SearchBox 클라 | 추가 검증 없음 | <code>isVisibleHit</code> 가드로 이중 방어 |
| 자동수집 빈도 | 매일 04:00 1 회 | 매일 04:00 + 16:00 2 회 |
| 검색 키워드 | 50 개 | 95 개 (애니/게임 IP / 럭셔리 / 디저트 / K-pop / 지역 추가) |
| Geocoding | 수동 호출만 | 매일 04:30 자동 cron (1차 수집 직후) |
| 자동 게시 ↔ 인덱스 | 연결 안 됨 (latent) | 신규/만료 즉시 Algolia 연동 |
| 재인덱싱 | 도구 없음 | <code>POST /api/admin/search/reindex</code> 신규 |
왜 이렇게 했음
이중 가드 — 인덱싱 단계에서 한 번, SearchBox 클라 렌더링 단계에서 한 번. 인덱싱만 가드하면 인덱스가 한 번 잘못 올라갔을 때 완전히 차단 불가. 양쪽 다 가드하면 운영자가 추후 수정하기 전까지는 사용자가 안 볼 수 있다.
자동수집 2 회의 이유 — 새 팝업 게시의 상당수가 오전·정오 이후의 대량 SNS 노출 흐름을 다음날 새벽에서야 잡을 수 있음. 16:00 한 번 더 돌리는 게 당일 게시 팝업이 당일 검색 결과에 올라갈 수 있는 유일한 방법.
95 키워드 — 50 개는 "서울 팝업 · 성수 팝업 · 강남 팝업" 수준의 최상위 키워드. 그 이하 관심사 (애니/게임 IP 점/테마 팝업/K-pop 아이돌 굿즈 팝업) 을 둘러서 수집 소스 다양성 보장. 95 개로 늘리면서 호출 횟수 증가는 LLM 호출 간격 (2.2 초) 와 Naver/Kakao 검색 간격 (800ms) 이 흡수.
Geocoding cron 분리 — 수집하자마자 좌표 변환하면 Kakao Local API 에 주소가 부정확해서 실패한 row 가 재시도 못 함. 04:30 에 따로 돌리니 1 차에서 설익 감 자세가 수집돼 있는 주소도 2 차 완성. 실패 row 는 admin 함릹에서 수동 개입.
코드로는 어떻게 (필요한 부분만)
인덱싱 가드.
파일: popspot-backend/src/main/java/com/popspot/search/AlgoliaIndexer.java
// v2.13 — AlgoliaIndexer.shouldIndex(). row 하나씩 인덱스 업로드 전 검사
private boolean shouldIndex(PopupStore p) {
var reviewOk = p.getReviewStatus() == null
// ^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ v1 이전 레코드 (구 스키마) 는 reviewStatus 컬럼 이 NULL → 허용
|| p.getReviewStatus() == ReviewStatus.AUTO_PUBLISHED
// ^^^^^^^^^^^^^^ confidence ≥0.8 에서 자동 게시된 것
|| p.getReviewStatus() == ReviewStatus.APPROVED;
// ^^^^^^^^ 관리자가 수동 승인한 것
// 반대 상태 PENDING/REJECTED 는 자동 포함 불가
var statusOk = p.getStatus() != Status.EXPIRED
&& p.getStatus() != Status.PENDING;
// ^^^^^^^^^^^^^^ 게시 상태 EXPIRED 와 PENDING 은 명확히 탈락
var conf = p.getConfidence() == null ? 1.0 : p.getConfidence();
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ null 이면 구 레코드 (수동 등록 등 confidence 가 없는 경우) 1.0 으로 간주·허용
return reviewOk && statusOk && conf >= 0.8;
// ^^^^^^^^^^^^ 세 조건 모두 true 이면 인덱스. 하나라도 안되면 제외
}SearchBox 클라 이중 가드.
파일: popspot-frontend/src/features/popup/SearchBox.tsx
// v2.13 — features/popup/SearchBox.tsx. 클라이언트 쪽 이중 가드
function isVisibleHit(h: any) {
// 위: h 는 Algolia 의 hit 객체. any 인 이유 — Algolia SDK 경계·에 한정됨 (v1.5 의 원칙)
if (h.status === 'EXPIRED' || h.status === 'PENDING') return false;
// 위: 서버 인덱스 가드가 뚫렸을 때의 최종 방어선
if (typeof h.confidence === 'number' && h.confidence < 0.8) return false;
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ number 타입일 때만 비교 — undefined 는 구 row 의 NULL 에 해당 그대로 통과 시키면 됨
return true;
}
results = hits.filter(isVisibleHit);
// ^^^^^^ 레이아웃 렌더링 직전 최종 필터cron.
파일: popspot-backend/src/main/java/com/popspot/service/crawler/PopupCrawlOrchestrator.java (v1.3 이후 지속 수정)
// v2.13 — 자동수집 cron. 매일 04:00, 16:00 두 번 수집
@Scheduled(cron = "0 0 4,16 * * *", zone = "Asia/Seoul")
// ^ ^ ^^^^ ^ ^ ^
// | | | | | +-- 요일 (전체)
// | | | | +---- 월
// | | | +------ 일
// | | +----------- 시 = 4 와 16 두 개 (쿼리 컴마로 구분)
// | +------------- 분 = 0
// +--------------- 초 = 0
public void scrapeTwice() { orchestrator.runOnce(); }
// ^^^^^^^^^^^^^^^^^^^^^^^ v1.3 의 동일 메서드 재사용. 설계 변경 없이 호출 빈도만 늘림
// v2.13 — Geocoding 자동화 cron. 04:30 에 NULL 좌표 row 채움
@Scheduled(cron = "0 30 4 * * *", zone = "Asia/Seoul")
// ^ ^^ ^ 30분 4시 — 1차 수집 후 30분 늦은 시점
public void geocodePending() { geocodingService.fillMissing(); }
// ^^^^^^^^^^^^ lat/lng 가 NULL 인 row 만 골라 Kakao Local API 호출·채움
// 수집 시점에 실패한 row 가 30분 안에 보완됨핵심 파일: popspot-frontend/src/features/popup/SearchBox.tsx, popspot-backend/.../service/crawler/PopupCrawlOrchestrator.java, popspot-backend/.../service/geocoding/KakaoGeocodingService.java, popspot-backend/src/main/resources/application.properties
v2.13.1~v2.13.3.1 팝스
| 버전 | 증상 | 수정 |
|---|---|---|
| v2.13.1 | 게스트 메인 진입 후 D-N 이 활성이지 않아서 잠시 깜빡. useEffect 의존성 누락 | 게스트 상태 구독 + 의존성 추가 |
| v2.13.2 | 약관 §10조의 2 이슐 (외부 검색 API 사용 형태) | 약관 문서 업데이트 |
| v2.13.3 | 어드민 아닌 계정이 /admin 진입 시 403 로그 도배 | AuthorizationDeniedException 을 GlobalExceptionHandler 에서 403 한 줄로 조용 처리 |
| v2.13.3.1 | 어드민 진입 시 인트로로 튀김 | middleware 의 admin 경로 예외 처리 추가 |
직접 보는 법
메인 상단 SearchBox 에 "팝업" 처럼 절반짜리 단어 입력 → 결과가 조용해졌다. 만료되었거나 운영자 검수 전 row 가 더 이상 안 보일 것. 권한 없는 상태에서 /admin 들어가 봐도 로그 도배·인트로 튀김이 없다.