o2o-negosium-original/lps/docs/result-states.md
민헌 04b697a9eb docs(lps): 검색 결과 상태 정의 — '못 찾음'과 '못 봄'을 가른다
'가격을 못 찾았다' 한 문장에 뜻이 정반대인 상황이 섞여 있다: 그 몰에 정말 없는 것(사실)과
그 몰이 우리를 막아 못 본 것(미확인). 지금 화면은 둘 다 '–' 로 똑같이 보여준다.
구현 전에 용어를 맞추려고 상태를 정의한다.

코드를 확인해 **실제로 구분 가능한 것**만 정의했다:

몰(소스) 단위 7상태 — 경계는 '사실'(no_match/empty) 대 '미확인'(blocked/env_blocked/unavailable).
앞의 둘은 "없다"고 말해도 되고, 뒤의 셋은 말하면 안 된다.

상품 단위 — found / not_found / error + partial 꼬리표. partial 은 독립 상태가 아니라
found·not_found 에 붙는데, 특히 'not_found + partial' 은 결론이 아니다(못 본 몰에 있었을 수 있음).

확인 과정에서 드러난 근본 원인:
- 어댑터는 이미 구분을 **안다** — AdapterError 에 blocked·fatal 이 있고 detect_block 이
  '차단 vs 정상 빈결과'를 판정한다.
- 그런데 **핸들러가 그 플래그를 버린다**: per_source[src] = {"error": f"{...}"} — 문자열만 남는다.
- 게다가 0건도 예외로 온다(return 은 1건 이상일 때만). 즉 empty 와 blocked 가 둘 다
  AdapterError 로 도착하는데 구분 플래그를 버리므로 이후로는 갈라낼 수 없다.

→ 새로 알아낼 정보는 없다. 이미 아는 걸 흘리고 있을 뿐이라, _search_round 한 곳이 출발점이다.
   남은 일을 1(플래그 보존) → 2(price_history 자리) → 3(화면 표기) 순서로 정리했다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 09:46:23 +09:00

6.6 KiB
Raw Blame History

최저가 검색 결과 상태 정의

← README로

가격을 못 찾았다는 한 문장 안에 뜻이 정반대인 상황들이 섞여 있다.

  • 그 몰에 정말 그 상품이 없다 → 사실이다. 사용자는 받아들이면 된다.
  • 그 몰이 우리를 막아서 못 봤다 → 사실이 아니다. 더 싼 값이 있었을 수 있다.

지금 화면은 둘 다 로 똑같이 보여준다(2026-08-06 확인). 그래서 사용자는 "쿠팡엔 더 싼 게 없구나"로 읽지만 실제로는 안 본 것일 수 있다. 이 문서는 그 구분을 위해 상태를 정의한다.

상태는 두 층이다. 몰(소스) 단위가 근본이고, 상품 단위는 그것을 합친 결론이다.


1. 몰(소스) 단위 상태

한 상품을 한 몰에서 찾을 때 나올 수 있는 결과. 괄호 안은 지금 코드가 그 상태를 아는지.

상태 사용자에게 재시도 의미
matched 수집·매칭 성공, 가격 확보 가격 표시 불필요
no_match 수집은 됐으나 같은 상품이 없음(액세서리·다른 규격만 나옴) "해당 상품 없음" 검색어를 고치면 달라질 수 있음
empty 그 몰의 검색 결과 자체가 0건 "해당 상품 없음" 검색어 의존
blocked 안티봇 차단 — 못 봤다 "확인 못함" 유효(IP 회전으로 회복)
env_blocked 회전해도 안 되는 차단(환경·게이트웨이 설정) "확인 못함" 사람이 고쳐야 함
unavailable 가용 IP 없음·프록시 사망 등 일시적 실패 "확인 못함" 잠시 후 유효
skipped 그 소스를 아예 안 씀(오픈마켓 폴백 OFF 등) 열 자체를 안 보임 해당 없음

핵심 경계는 no_match/empty(= 사실) 와 blocked/env_blocked/unavailable(= 미확인) 사이다. 앞의 둘은 "없다"고 말해도 되고, 뒤의 셋은 말하면 안 된다.

지금 코드가 아는 것 / 잃는 것

어댑터는 이미 이 구분을 알고 있다. AdapterErrorblocked·fatal 플래그가 있고, 0건일 때 detect_block 으로 '차단인가 정상 빈결과인가'를 판정한다(browser_base.py).

0건 응답 → detect_block(html, ...) → marker
   marker 있음 → blocked=True   (fatal 마커면 fatal=True)
   marker 없음 → blocked=False  ← '정상 빈결과' = empty

그런데 핸들러가 그 플래그를 버린다:

# worker/handlers.py — _search_round
per_source[src] = {"error": f"{type(res).__name__}: {res}"}   # ← blocked/fatal 유실, 문자열만 남음

⚠️ 그리고 0건도 예외로 온다(return 은 상품이 1건 이상일 때만). 즉 emptyblocked 가 둘 다 AdapterError 로 도착하는데, 구분 플래그를 버리므로 핸들러 이후로는 갈라낼 수 없다.

상태 어댑터가 아는가 핸들러가 아는가 price_history 에 남는가
matched by_mall 에 등장
no_match (수집>0, 매칭 0) 유도만 가능, 기록 없음
empty blocked=False 유실
blocked blocked=True 유실
env_blocked fatal=True 유실
unavailable 별도 메시지 유실

고칠 지점은 한 곳이다: _search_round 가 플래그를 구조화해 넘기고, 그게 price_history 까지 가면 된다. 새로 알아내야 할 정보는 없다 — 이미 아는 걸 흘리고 있을 뿐이다.


2. 상품 단위 상태

몰별 상태를 합친 결론. price_history.outcome 이 이 값이다.

상태 조건 현재 지원
found 매칭 1건 이상 최저가 확정
not_found 모든 소스가 사실 응답(no_match/empty)이고 매칭 0 확정적 없음
error 모든 소스가 미확인(blocked 계열) 아무것도 확인 못함 (2026-08-06 추가)
partial 일부 소스만 미확인 아래 참고 ⚠️ 부분 지원

partial 이 중요한 이유

partial 은 독립된 상태가 아니라 found/not_found 에 붙는 꼬리표다. 조합이 넷인데 사용자에게 주는 의미가 전혀 다르다.

조합 사용자에게 할 말
found 전 소스 확인, 최저가 확정 "최저가 N원"
found + partial 찾긴 했지만 못 본 몰이 있다 "최저가 N원 (쿠팡은 확인 못함)"
not_found 전 소스 확인, 확정적 없음 "찾지 못했습니다"
not_found + partial 결론이 아니다 — 못 본 몰에 있었을 수 있다 "확인하지 못했습니다"

특히 마지막이 위험하다. 이걸 not_found 로 뭉뚱그리면:

  • 사용자는 "없구나"로 오해하고
  • 네거티브 캐시에 들어가면 TTL 동안 재검색까지 막힌다

→ 그래서 부분 실패 상태의 not_found 는 네거티브 캐시에 넣지 않는다(2026-08-06 반영). partial 플래그 자체는 job.result 에 있지만 price_history 에는 없어 화면까지 가지 못한다.


3. 화면 표기 매핑

몰 열 조건
가격 matched
no_match / empty사실로서 없음
확인 못함 blocked / env_blocked / unavailable미확인
결과 열 조건
탐색 성공 found 이고 기존보다 쌈
변동 없음 found(더 싸진 않음) 또는 not_found
일부 확인 못함 partial — 결과가 확정이 아님을 알린다
탐색 실패 error — 전부 미확인
탐색 중 아직 결과 행이 없음

4. 현재 상태와 남은 일

된 것

  • error(전부 미확인)를 not_found 와 분리 — 화면이 "탐색 실패"로 구분해 보여준다
  • 부분 실패 시 네거티브 캐시 오염 방지
  • 한 소스가 막혀도 살아있는 소스로 잡을 정상 종료

안 된 것

  1. _search_roundAdapterError.blocked/fatal 을 버린다 → 몰별 상태를 못 만든다 (가장 근본)
  2. price_history 에 몰별 상태·partial 을 담을 자리가 없다 → negodata 가 못 읽는다
  3. 화면이 (사실로서 없음)와 확인 못함(미확인)을 구분하지 못한다

1 → 2 → 3 순서로 풀어야 한다. 1을 안 고치면 2·3은 담을 내용이 없다.

이 문서는 정의다. 구현 전에 용어를 맞추기 위한 것이고, 실제 반영 여부는 위 4절이 소스다.