Commit Graph

385 Commits

Author SHA1 Message Date
민헌
8fb54be830 feat(lps): local/dev/prod 환경별 실행 체계 — 환경 toml 2종 + 대화형 run_docker.sh
다른 개발자가 어느 환경이든 같은 방식으로 띄울 수 있게 한다.
환경 = APP_ENV 가 고르는 config.<env>.toml 하나(구조 동일, 값만 다름).

- config.docker.toml → config.dev.toml 개명, config.prod.toml.example 신설
  (prod 성격 반영: api_keys 필수 표기·debug 로그 금지·웹훅 권장).
- compose 의 lps 서비스 APP_ENV/마운트를 ${APP_ENV:-dev} 로 파라미터화,
  API 포트를 ${LPS_API_BIND:-0.0.0.0} 바인드로 노출 제어.
- run_docker.sh(대화형) 신설: 환경 선택 → 설정 파일 검증(없으면 example
  복사 제안) → 기동/재시작/중지/로그. prod 는 guard 키 비면 경고 후
  확인받고, LPS_API_BIND=127.0.0.1 자동 설정(외부는 리버스프록시 경유).
- 운영 가이드에 환경 개요 표, README 빠른 시작에 Docker 실행 추가.
- 검증: 스크립트 문법·compose(dev/prod) 파싱·APP_ENV=dev 로딩·145 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 21:22:53 +09:00
민헌
c81df5bd88 refactor(lps): 설정을 TOML 단일 소스로 통합 — env/.env 이중 관리 제거
설정이 .env(compose 주입)·config.toml·코드 곳곳의 os.environ 직독 3계층에
흩어져 관리가 어려웠다. TOML 하나로 통합한다(협의 결정).

- 신설 [WorkerConfig](동시성·폴백·프로필·데드라인·유예·Chrome·하트비트),
  [AlertConfig](웹훅·쿨다운·임계 10종). [WebServerConfig].api_keys(guard),
  [DecodoConfig].ip_request_budget/port_cooldown_sec 추가 — 흩어져 있던
  LPS_* env 20여 개를 섹션으로 흡수.
- server_configs 의 env override 계층(DB_*·시크릿·NAVER_KEYS 등) 삭제.
  남는 env 는 APP_ENV(부트스트랩)·PROCESS_COUNT/WORKER_CONCURRENCY(실행
  스크립트 대화형 입력 전용)·LPS_LIVE(테스트 옵트인)뿐.
- Docker: env 주입 → config.docker.toml 마운트 + APP_ENV=docker.
  이미지 무시크릿 유지, 마운트 누락 시 FileNotFoundError 즉시 실패.
  .env.example 삭제, config.docker.toml.example 신설.
- negodata 호출부: guard 키를 env 직독에서 [WebServerConfig].lps_api_key
  (+기존 관례대로 env override)로 이동.
- 실행 스크립트: 프로필·폴백·예산 프롬프트 제거(toml 소스 안내),
  동시성/프로세스 수만 임시 override 로 유지.
- docs 7종·example toml 의 env 표기를 toml 키로 일괄 갱신.
- 전체 145 passed + APP_ENV=docker 로딩·API 기동 스모크 확인.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 21:11:31 +09:00
민헌
993301be8b docs(lps): README·docs 최신화 — IP 선제 회전·알림 10룰·guard·ip_session 반영
이번 기능 3종+알림 확장 이후 문서와 코드의 어긋남을 정리한다.

- README: 주요 기능 표에 선제 회전(예산 3회)·임계 알림·API guard 추가,
  차단 대응 설명을 '막히기 전 교체' 순서로 재서술, 폴더 구조에
  alerts/ip_session 반영, 데이터베이스 문서 링크를 5종으로 수정.
- api.md: /v1/lps/ops 운영 스냅샷 섹션 신설(필드 주석 포함),
  /readyz 문서화, HTTP 401(guard) 상태 코드 추가.
- architecture.md: 구성요소 표에 관측·알림/API guard 행 추가.
- database.md: 제목 '테이블 5종'으로 수정.
- operations.md: 테스트 수 96→145, 로그 읽는 법에 예산 선제 회전·
  포트 쿨다운 로그 2행 추가.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 20:46:59 +09:00
민헌
ca7f057e41 feat(lps): 알림 룰 4종 추가 — 데드라인·검색원가·포트 고갈·예산 누수
협의로 선정한 조기 신호 4종을 AlertManager 에 추가한다.

- deadline: 최근 1h JobDeadlineExceeded 수 ≥ LPS_ALERT_DEADLINE_1H(5).
  재시도로 살아나면 dead 룰엔 안 잡히는 크롤 행 반복 신호를 별도 집계.
- cost: 최근 1h 완료 잡 검색원가 합 ≥ LPS_ALERT_COST_1H_USD(1.0).
  비용의 87%가 프록시 대역폭 — 리소스차단 풀림·재시도 루프의 조용한
  비용 폭주를 감시. job.result 의 metrics.cost.total_usd JSONB 합산.
- proxy_ports_low: 가용 포트 비율 ≤ LPS_ALERT_PORTS_LOW_PCT(30%).
  쿨다운 격리 누적 — blocks_1h(80건)보다 먼저 우는 대규모 차단 조기
  신호. 워커별 프록시 중 가장 소진된 것 기준(min).
- budget_leak: 최근 6h end_reason=block 세션 ≥ LPS_ALERT_BLOCK_SESSIONS_6H(1).
  요청 예산(3회)을 지켰는데도 차단됨 = 예산 하향 검토 신호.
- deadline_1h·cost_1h_usd 는 queue.ops() 에 편입 → /v1/lps/ops 로도 노출.
  포트·세션 지표는 워커 웹훅 스냅샷에 포함(프록시 상태는 워커에만 있음).
- 테스트 4건 추가(ops 집계 2·포트 스냅샷 2), 전체 145 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 17:29:08 +09:00
민헌
e7d2c87fbe feat(lps): API guard — LPS_API_KEY 설정 시에만 /v1 에 X-API-Key 검증
외부에서 API 를 함부로 호출(비용 발생 enqueue 등)하지 못하도록 정적 키
guard 를 추가한다. '키의 존재'가 토글 — 개발(local/dev)은 env 를 비워
개방 모드(기동 시 WARN), prod 만 키를 주입한다(협의 결정).

- router/v1/validator/auth.py: X-API-Key 의존성 — secrets.compare_digest
  상수시간 비교, 콤마 구분 복수 키(무중단 키 교체), 매 요청 env 조회
  (재기동 없이 테스트 가능). /v1 라우터 전체에 적용.
- /healthz·/readyz 는 라우터 밖이라 항상 개방(LB 프로브).
- negodata lps_sync_service: LPS_API_KEY env 있으면 헤더 자동 첨부(한 곳).
- compose(lps-api·negodata-backend) LPS_API_KEY 패스스루 + .env.example.
- prod 체크리스트(operations.md): 키 주입 + lps-api 포트 비공개 + 기동
  로그 'API guard ON' 확인. api.md 인증 섹션 추가.
- 라이브 스모크: 무헤더/오키 401 · 정키 2종 200 · healthz 200 확인.
- 테스트 6건 추가, 전체 141 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 16:45:31 +09:00
민헌
8b9e81777c feat(lps): 알림 확장 — AlertManager 쿨다운·회복, DB풀 포화·소스별 장기실패 룰
기존 ops-monitor 는 임계 초과가 지속되면 30초마다 같은 웹훅을 반복 발송했고
(쿨다운 없음), 해소 여부도 알 수 없었다. 감시 항목도 큐 지표 4종뿐이었다.

- common/alerts.py AlertManager 신설: 룰 키별 상태 관리 — 발화 1회 +
  쿨다운(LPS_ALERT_COOLDOWN_MIN, 기본 30분)마다 리마인드, 해소 시 회복
  알림 1회. sender/clock 주입으로 네트워크·대기 없이 단위 테스트.
- 워커 ops-monitor 를 AlertManager 로 이관(기존 4룰 유지) + 신규 2룰:
  db_pool(풀 포화율 ≥ LPS_ALERT_POOL_PCT 90%) ·
  source_fail:<src>(최근 30분 시도 ≥ LPS_ALERT_SOURCE_FAIL_30M(5) & 성공 0
  — 쿼터 소진·셀렉터 드리프트·전면 차단 신호).
- DBSessionManager.pool_status(): 전 엔진 합산 checked_out/capacity/pct.
- SearchAdapter 에 시간 윈도우 성공/실패 카운터(recent_stats) — 누적
  카운터로는 '최근 30분 성공 0건'을 볼 수 없어 추가. 쿠팡(브라우저)·
  네이버(API) 성공/실패 지점에 배선.
- API 자체 풀 모니터: lifespan 백그라운드 태스크(run_pool_monitor) —
  대량 폴링으로 풀을 고갈시키는 주범이 API 자신일 수 있다.
  /v1/lps/ops 에 pool_checked_out/pool_capacity/pool_pct 노출(스모크 확인).
- 테스트 9건 추가(발화·쿨다운·회복·룰 독립·윈도우 카운터·풀 현황), 전체 135 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 16:42:12 +09:00
민헌
3c5286aeec feat(lps): IP 선제 로테이션 — 요청 예산·포트 쿨다운·ip_session 관측
쿠팡 크롤 IP 를 '막힐 때까지' 쓰던 방식을 '막히기 전에 교체'로 전환한다.

- 요청 예산(LPS_IP_REQUEST_BUDGET, 기본 3): IP당 요청 수가 예산에 닿으면
  차단 전에 선제 회전. 실측상 5회 부근 차단 이력이 있어 보수적으로 3회.
  선제 교체된 포트는 평판이 깨끗해 로테이션 복귀 시 재사용된다.
- 포트 쿨다운(LPS_PORT_COOLDOWN_SEC, 기본 max(sticky,30분)): 차단 감지·
  전송오류 포트는 격리하고 _port() 가 건너뛴다. 전 포트 쿨다운이면 만료
  임박 포트 사용(가용성 우선). 포트 수는 config 범위에서 동적 산출.
- 차단 재시도 소진 시에도 회전 예약 — 불탄 포트로 다음 검색을 하지 않음.
- ip_session 테이블 신설: 세션마다 요청 수·성공/차단·종료 사유(budget/
  block/proxy_error/window/idle/shutdown)를 기록. bot_detection 과 달리
  무사 종료도 남아 예산 상한 튜닝의 원천 데이터가 된다(쿼리 database.md).
  models.py·migrations·init.sql(lps_db 섹션) 동행 갱신, dev DB 적용 완료.
- 테스트 17건 추가(쿨다운·예산 판정·세션 기록·CRUD), 전체 126 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 16:35:44 +09:00
민헌
f97e407f7c Merge branch 'main' into feature/negodata-lps 2026-07-13 13:55:36 +09:00
21f90ed84a [fix] negodata/front: 윈도우 브라우저 폰트 미적용 수정 — format woff2-variations 를 표준 woff2 로 교체 2026-07-13 08:47:09 +09:00
ac487268d8 postgres-init lps db 추가. 2026-07-10 17:19:24 +09:00
민헌
9ef9bfbca3 feat(negodata/front): 최저가 상세보기 버튼 노출 + 상세 시트 가독성 정리
- 상품 테이블 인터넷 최저가 셀: 값 옆에 차트 아이콘 버튼 상시 노출
  (기존 hover 밑줄만으론 클릭 가능성이 안 보임) — 값 없는 상품도
  버튼으로 진입해 빈 상태 안내를 받는다
- 상세 시트 재구성: 견적 상세 드로어와 같은 SectionCard 문법으로 통일
  · 헤드라인 카드: 상품명 → 대표가(26px rose) + 단가 대비 칩
    (절감=emerald·역전=amber, Trending 아이콘) + 단가·마지막 수집 캡션
  · 최저가 출처: 섹션 타이틀 우측 사이트 배지, 링크는 outline 버튼화
  · 가격 추이: 수집 횟수 캡션, 2회 미만이면 안내 문구(빈 영역 방지)
  · 수집 이력: 배지 폭 고정(w-14)으로 컬럼 정렬, 링크 없는 행 자리 유지

검증: 헤드리스 렌더 확인(테이블 셀·시트 v2 스크린샷), tsc 통과

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:18:09 +09:00
민헌
4a20194dc8 feat(negodata): 인터넷 최저가 출처·링크·가격 추이 UI — 검증 가능한 협상 근거
인터넷 최저가는 견적 목표가·협상 카드(NGC-008 시장가 근거)의 근거값
— "어디서 찾았는지"를 클릭 한 번으로 검증할 수 있어야 한다.

- 스키마: iilp 에 lp_name(찾은 상품명)·lp_url(판매 페이지 링크) 추가
  (alters/2026-07-10-iilp-source-link.sql, init.sql 멱등 반영·dev 적용)
- 동기화: price_history 의 final_source 에 맞는 naver/coupang name·url
  을 함께 반영(기존 행은 NULL 공존)
- API: LowestPriceEntry 에 lp_name/lp_url + orval 재생성
- UI: 상품 테이블 인터넷 최저가 셀 클릭 → 상세 시트
  · 대표가(rose) + 상품 단가 대비 차액(절감=emerald/역전=amber)
  · 출처 카드: 사이트 배지·찾은 상품명·판매 페이지 새탭 링크
  · 가격 추이 라인 그래프(성공 수집 2건부터, recharts+ChartContainer
    라이트/다크 쌍 — 통계 차트와 동일 문법)
  · 최근 수집 이력 리스트(미발견 amber, 행별 외부링크)

검증: 실데이터 e2e(A4 재수집 → lp_name/lp_url 반영 확인) +
헤드리스 브라우저 렌더 확인(시트·그래프·툴팁·링크), tsc·build 통과

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:10:00 +09:00
민헌
1cce752b47 Merge branch 'feature/negodata-lps' — negodata↔LPS 인터넷 최저가 연동(수동 트리거 전용) 2026-07-10 16:49:49 +09:00
민헌
5ddf5f6249 refactor(negodata): LPS 자동 요청 배치 제거 — 검색은 수동 트리거 전용
협의 결정(2026-07-10): 크롤 비용이 사용자 행동에만 비례하도록,
갱신 오래된 상품을 자동으로 검색 요청하던 잡(매일 04:00, 상한 500건,
일 ~$2)을 제거한다. 검색 진입점은 상품 화면의 수동 트리거
(POST /v1/item/{id}/lowest-price) 하나만 남는다.

- request_lps_searches 잡·request_stale_searches 서비스·stale_items
  CRUD·배치 상수(REFRESH_HOURS 등) 제거
- 수집 잡(sync_lps_results, 5분)은 유지 — 크롤을 일으키지 않는
  반영 백스톱(모달 조기 종료·폴링 초과분 자동 반영, 비용 0)

검증: 스케줄러 등록 잡 3개(견적마감 2 + LPS 수집 1) 확인, 수집 잡 실행 정상

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:47:39 +09:00
민헌
3171311746 feat(negodata/front): PriceUpdateModal 데모 → LPS 실연동
- 가짜 setTimeout 애니메이션 제거 — 상품별 POST lowest-price(큐 접수,
  duplicated 는 결과 대기 합류) 후 GET 5초 폴링(서버가 조회 시 lps_db
  증분 동기화를 겸하므로 폴링이 곧 수집)
- 요청 시각 이후 crawl_end_time 이력만 이번 결과로 인정, 완료/미발견
  로그 구분(emerald/amber), 5분 대기초과분은 주기 동기화 자동반영 안내
- 종료 시 /v1/item/list invalidate → 테이블 인터넷 최저가 컬럼 갱신
- 닫기 = 폴링만 중단(서버 검색은 계속됨을 UI 에 명시)
- orval 재생성: lowest-price 응답 타입화(LowestPriceEntry·lowest_price)

검증: tsc 통과, vite build 통과 (API 흐름 자체는 ③ e2e 로 검증됨)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:32:38 +09:00
민헌
28b4a9e797 feat(negodata): lowest-price 스텁 API 실구현 — 즉시 트리거 + 온디맨드 동기화 조회
- POST /v1/item/{id}/lowest-price: 상품 1건 LPS 즉시 검색요청(manual
  우선순위). queued/duplicated/unavailable 상태 반환, 실패는
  ErrorType.LPS_UNAVAILABLE(2000 블록 신설)
- GET /v1/item/{id}/lowest-price: 대표 최저가 + 최근 수집 이력(최신순,
  LowestPriceEntry 타입화). 조회 전 lps_db 증분 동기화 1회 수행 —
  5분 크론을 기다리지 않는 실시간 폴링 UX(멱등·저비용)
- 동시성 방어 2겹: 프로세스 내 asyncio.Lock(크론·온디맨드 직렬화) +
  uq_iilp_item_crawl_time 유니크 인덱스(alters/2026-07-10, init.sql
  멱등 반영·dev DB 적용) — 경합 진 쪽 tx 실패 후 다음 tick 흡수
- LpsSyncService 무인자 생성자(FastAPI Depends 호환)
- compose: negodata-backend 에 LPS_DB_HOST·LPS_BASE_URL env(미설정 시
  연동 비활성으로 조용히 동작)

검증(도커 컨테이너 e2e): 인증→POST queued→LPS 크롤→GET 온디맨드
동기화로 이력 즉시 노출(not_found 정책: 대표값 미변경 확인),
company 스코프 차단(타사 상품 ITEM_NOT_FOUND) 확인

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:17:05 +09:00
c0e6987c3c [fix] negodata/front: 통계 카테고리별 절감 차트 -> 손해(음수)는 안보이게, 대시보드에서 일반관리자도 회사 통계 보이게 수정 2026-07-10 15:58:13 +09:00
민헌
523661e3d2 feat(negodata): LPS 최저가 동기화 배치 — 요청(API)·수집(lps_db)·반영(items)
설계(2026-07-10 결정): 요청은 LPS API(dedupe·워커알림 보존), 결과는
lps_db.price_history 직접 읽기(읽기전용 엔진). product_code=item_id
로 결과가 자동 매핑된다.

- 잡③ request_lps_searches(매일 04:00 KST): 24h 이상 미수집 상품을
  최대 500건 enqueue(job_type=batch, 100건/콜 청크) — 비용 발생 잡
- 잡④ sync_lps_results(5분): 워터마크(max crawl_end_time) 증분 수집
  → item_internet_lowest_prices append(성공/실패 모두) + 성공분 최신값
  items.internet_lowest_price 박제(+yn). 한 트랜잭션(부분반영 방지)
- 스캔 바닥 3일 — 비uuid·미존재 상품 행이 워터마크를 못 올려도
  재스캔 범위 유한
- ⚠️ 시각은 aware UTC 통일 — naive 를 timestamptz 파라미터로 넘기면
  PG 세션 타임존(KST) 해석으로 9시간 어긋남(중복 수집 실측 버그 수정)
- LowestPriceWebsite 코드 enum(naver=1 coupang=2 …), iilp ORM 모델,
  lps_base_url config(+LPS_BASE_URL env)

검증: 실상품 1건 e2e(요청→크롤 found 15,000원→이력+박제 반영),
멱등성(재실행 빈 카운터), 비uuid 12건 스킵, 대상선정 쿼리 500건 상한

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 15:38:57 +09:00
b41f7c67f1 [feat] negodata: 카드 에디터 조건 전략 변수 + 미리보기 변수 라벨 치환
① 카드 목록 스크립트 미리보기에서 {변수} 토큰을 한글 라벨 칩으로 렌더
   (renderCardScriptPreview) — 원시 토큰({prev_partner_price} 등) 노출 제거.
② "조건 전략"(customer_condition) 특수 변수 추가:
   - 툴바 '+ 조건 전략' → 본문에 위치 칩 삽입, 본문 아래 별도 Slate 입력창 노출
   - 조건 내용은 변수 노드에 중첩 보관(edit_script 배열 유지·하위호환) + 저장 시
     script 의 해당 위치에 주입 → agent 는 완성된 문구를 그대로 읽음(무변경)
   - 서브 에디터는 조건 전략 버튼 숨김(중첩 방지), 빈 조건 저장 차단

DB·백엔드·agent 변경 없음(edit_script 타입 unknown, 조건 노드 내 중첩).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 15:18:11 +09:00
민헌
9e9de52200 feat(negodata): lps_db 읽기전용 연결 기반 — DBType.LPS + 조건부 엔진 등록
LPS(인터넷 최저가 검색) 연동 1단계. price_history 동기화 배치가 쓸
읽기전용 접속을 기존 확장점(DBType enum + 세션매니저 맵)에 등록한다.

- DBType.LPS=2, LpsDBConfig(read_* 만 — write 엔진 미등록으로 앱 차원
  읽기전용 강제, write 요청 시 KeyError)
- config name 비면 미등록(구버전 toml·LPS 없는 환경에서도 부팅 가능),
  배치는 is_registered() 로 사전 확인
- env override: LPS_DB_HOST/PORT/USER/PASSWORD/NAME (도커용)
- 검증: 엔진 등록·읽기 세션·write 가드 KeyError·lps_db.price_history
  실조회 확인

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 15:11:18 +09:00
26d9fa3c4b Merge branch 'feat/chat-update' — 협상 고도화(LLM 표현/이해층·카드 전술·변수 카탈로그) 2026-07-10 13:39:19 +09:00
22500eae4c [chore] compose: agent 에 OPENAI_API_KEY passthrough — LLM 표현/이해층 키 주입
빈 값이면 무시(server_configs env override 가 값 있을 때만 적용)라 무해.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 13:37:50 +09:00
afacd74382 [feat] negodata: 카드 변수 카탈로그에 전술 변수 6종 추가
agent 전술 재설계로 늘어난 치환 변수를 편집기 툴바·칩에 노출:
제시가(input_price)·협력사 직전가(prev_partner_price)·제안가(counter_price)·
중간가(target_mid_price)·절충가(middle_price)·인하율(discount_rate).
name 은 agent chat_engine.vars_for 치환 키와 일치. 라벨은 렌더 시점 조회라
기존 저장 카드도 마이그레이션 없이 한글 라벨로 표기된다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 13:37:50 +09:00
1682481b01 [feat] agent: 협상 고도화 — LLM 표현/이해층 + 카드 전술 실행계층 + ktcommerce 정리
카드 재설계("멘트 카드 → 전술 카드"):
- tactics.py 신설: 카드번호→전술(카운터 산식) 레지스트리, min(counter,target) 클램프
- 카운터 수락=즉시 타결(pending_counter_price 일반화, 구 offer_1pct 흡수)
- 목표가 초과 타결 금지(성공스텝 진입 가드) + "카드 소진=실패" 폐지→종결 국면
- 선택형 와일드카드(WC-*) 발동 + card.wild_cards 멘트 DB 어댑터

LLM 계층:
- Phase 2 표현층 ScriptNaturalizer(카드 멘트 자연화, 마커·치환자·숫자 보존 검증)
- Phase 3 이해층 InputInterpreter(자유발화 NLU→기대입력, 한국어 가격 파서)
- OPENAI_API_KEY env override(server_configs) + 전역 자격증명 게이트

결정 스택(Phase 1):
- 협상 규칙 데이터화(negotiation.wildcard_*_ratio/max_counter_rounds)
- 선택카드 우선순위 prior(UCB 방문수 감쇠, Q-table 오염 없음)

버그픽스:
- 인하율 음수 표기 제거 + 인상/동일/인하 구분(discount_phrase)
- 자연화 강조마커 보존(볼드/색 소실 시 원본 폴백)
- 카드 시드 가격변수(prev_partner_price·target_mid_price·middle_price 등) 치환

정리:
- ktcommerce 테넌트 삭제 + 테스트 21파일 imarketkorea/_base 로 마이그레이션
- 실 LLM 호출 차단 conftest 가드

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 13:37:36 +09:00
민헌
18f6466a8b Merge branch 'feature/lps' — 인터넷 최저가 검색(LPS) 시스템 2026-07-10 13:30:48 +09:00
민헌
d4cee1caf7 feat(lps): API 스케일 파라미터를 .env 로 노출 — PROCESS_COUNT·커넥션 예산
주석 상태였던 PROCESS_COUNT/DB_CONNECTION_BUDGET 를 활성화하되 서버별
값은 .env(LPS_API_PROCESS_COUNT, LPS_DB_CONNECTION_BUDGET)로 조정.
기본 1/40 — API 는 단일 프로세스로도 ~1,100 RPS(실측)라 평시 충분,
부하 대비 시에만 상향. 풀 자동 산정이 예산 초과를 방지.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 12:02:17 +09:00
민헌
a267326bf7 fix(lps): Docker 이미지 무시크릿화 + 배포 견고화 — 리뷰 발견사항 일괄 적용
시크릿 유출 차단(핵심):
- config.local.toml(OpenAI·DECODO·네이버 키)이 COPY . . 로 이미지에
  구워지던 문제 — .dockerignore 제외 + 빌드 시 example(플레이스홀더)
  복사로 대체. 실값은 compose env 주입(리포 루트 .env, 템플릿 .env.example)
- DECODO_PORT_START/END/SESSION_MINUTES env override 추가 — 포트가
  toml(플레이스홀더 0)에만 있으면 자격증명을 넣어도 프록시가 조용히
  꺼지는 구멍 봉합

배포 견고화:
- API Dockerfile 에 HEALTHCHECK(/healthz) 추가
- autoheal 컨테이너 추가 — compose restart 는 unhealthy 를 재시작하지
  않으므로 라벨(autoheal=true) 기반 자동 재시작 담당
- 이미지 python 3.12→3.14 정렬(로컬 개발·테스트 환경과 일치)
- API 이미지 경량화: requirements-api.txt 분리(크롤 의존성 제거, 330MB)

검증: 양 이미지 빌드 성공, 이미지 내 시크릿·.profiles 부재 확인,
무시크릿 API 이미지 스모크(healthz/readyz/HEALTHCHECK healthy) 통과

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 11:53:47 +09:00
민헌
c075dfbcfb fix(lps): .dockerignore 에 .profiles/ 추가 — Chrome 세션 이미지 유출 차단
COPY . . 가 로컬 Chrome 프로필(쿠키·cf_clearance, 149MB)을 API·워커
이미지에 굽고 있었다. 컨테이너는 빈 프로필에서 웜업으로 쿠키를 만들고
volume(/profiles)에 영속하는 설계라 이미지에 있을 이유가 없다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 11:45:19 +09:00
민헌
4381649658 docs(lps): 워커·브라우저 1:1 근거와 워커 수 상한 산정법 문서화
- 6-2: 브라우저가 아니라 신원(프로필+sticky IP)이 희소 자원인 이유,
  비율이 어긋날 때의 손해(유휴 낭비 vs 락 직렬화), 신원당 처리량 캡
- 6-3: 워커 상한 = min(RAM캡, 포트캡÷3, 실측 정체점) — 하드캡 계산식과
  계단식 실측 시 멈춤 신호 5가지(처리량 정체·blocks_1h·메모리·
  단일코어 포화·p95)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 11:32:12 +09:00
민헌
463416c3a2 fix(lps): 워커 행(hang) 3중 방어 — 잡 데드라인·웜업 타임아웃·정리 루프 격리
부하테스트(N=10) 실측: 쿠팡 브라우저 재기동 경로 행으로 워커 2/3가
12~15분 정지(쿠팡 검색 1콜 880s), heartbeat 가 lease 를 계속 갱신해
reaper 회수 불가 + stuck_running=0 으로 관측 사각. 이에 대한 방어:

- P1 잡 데드라인: runner._process 에 wait_for(기본 300s, env
  LPS_JOB_DEADLINE_SEC) — 초과 시 취소·fail → 백오프 재큐/DEAD
- 웜업 시도당 60s 타임아웃 — 웜업이 어댑터 락을 쥔 채 행하면
  그 워커의 실 검색이 전부 락 대기로 동반 정지(실측 케이스)
- P2 browser reaper: close_if_idle 에 60s 타임아웃 — 한 어댑터
  close 행이 정리 루프 전체를 멈추는 것 방지(실측 케이스)
- P3 stuck_running: lease 만료 OR 실행 10분 초과(run_started_at)
  — 행 상태에서도 지표·알림에 잡히게

테스트 5건 추가(데드라인 DEAD/재큐, stuck 지표, reaper 격리)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 11:27:03 +09:00
민헌
fbd2cec1da test(lps): 부하테스트 상품셋 100종 카탈로그 분리 — 실제 입력 분포 재현
- loadtest/catalog.json 신설(실존 상품 100종): 규격만 54 · 모델포함 26 ·
  이름만 20 · 기준가 포함 7 (price→가격밴드 필터 경로 포함)
- loadtest.py 는 인라인 6종 대신 카탈로그 로드, 실행 시 구성 요약 출력

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 10:50:28 +09:00
민헌
5e485e9b6e feat(lps): 오픈마켓 폴백 기본 비활성(LPS_FALLBACKS 토글) — 협의 결정 B안 적용
2026-07-10 협의: 검색은 네이버+쿠팡만. G마켓·옥션·11번가 폴백은 최종 최저가
기여 0회에 검색당 최대 15s·프록시 비용 ~87%를 차지해 로직에서 제외.
주석처리 대신 env 토글로 코드·테스트는 살려둔다(부패 방지·env 한 줄 재가동).

- worker_main: LPS_FALLBACKS(기본 빈값=OFF)로만 폴백 어댑터 생성, 잘못된 값 경고,
  기동 로그에 폴백 상태 표기. 핸들러는 빈 폴백을 원래 정상 처리라 로직 변경 없음
- run_local_worker.sh: 폴백 여부 대화형 질문 추가(기본 비활성)
- compose: LPS_FALLBACKS 주석 env(재가동용)
- decision-openmarket-crawler.md: 결정(B)·근거·재가동 절차(라이브 스모크 선행) 확정 기록
- README·architecture·operations: 기본 비활성 반영

검증: 전체 테스트 106 passed(폴백 로직 테스트는 fake 주입이라 계속 유효),
워커 실기동 로그 '오픈마켓 폴백: OFF' 확인

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 10:14:39 +09:00
7f1c0be2b3 [feat] negodata: 협력사 채팅 계정(supplier_users) 관리 — 발급·비번 재설정·활성상태
- 백엔드: supplier_users/supplier_user_tokens ORM 매핑, /v1/supplier/{id}/account 4종(조회·발급·재설정·상태), 회사 스코프 게이팅, 재설정·비활성 시 토큰 삭제로 기존 로그인 즉시 무효화
- 비밀번호: 발급=서버 자동생성(1회 반환), 재설정=커스텀 지정 가능(미지정 시 자동생성)
- 프론트: 협력사 상세 SupplierAccountManager 섹션 + 목록 '채팅 계정' 컬럼, orval 재생성(SupplierType 은 enumLabels 로컬 상수로 이동)
- 테스트: test_supplier_account.py 9건, 전체 스위트 green
2026-07-10 10:14:00 +09:00
민헌
89fe2c9805 feat(lps): 실시간 관측 대시보드 추가 — 큐 추이·코어별 CPU·프로세스 그룹 사용률
loadtest/monitor.py (:9700, 단일 파일·외부 인프라 없음) + run_monitor.sh(대화형).
e2e 부하(N=100 loadtest.py) 중 "프로세스가 잘 진행되는지, 코어가 전부 도는지,
병목이 어느 층인지"를 브라우저에서 2초 간격으로 본다:

- 큐 추이: /v1/lps/ops 폴링 — PENDING/RUNNING/DONE/DEAD 라인 + 처리량(개/분) 타일
- 프로세스 그룹 CPU: worker/api/chrome/postgres — 병목 층 판독
  (chrome 은 워커 자손만 집계해 사용자 브라우저와 분리, uvicorn spawn 자식은 부모로 api 귀속)
- 코어별 사용률 막대: 멀티코어 활용 확인
- 현재 스냅샷 표 + 호버 툴팁 + 라인 끝 직접 라벨(dataviz 팔레트 검증 통과, 다크 서피스)
- psutil 의존성 추가(로컬 관측 전용)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 09:50:14 +09:00
민헌
64d5063041 feat(lps): 서버 실행 스크립트에 멀티코어 대화형 선택 추가
일반 실행(1) 선택 시 PROCESS_COUNT 를 물어보고(기본 1, CPU 코어 수 표기),
2 이상이면 DB_CONNECTION_BUDGET 도 물어본다(기본 96 — 전용 PG 벤치값).
워커 스크립트(run_local_worker.sh)의 동시성 질문과 같은 대화형 패턴.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 09:09:55 +09:00
민헌
47c64eaf8b fix(lps): 워커 graceful shutdown — 신호 핸들러·잡 마무리 유예·정리 격리
- SIGINT/SIGTERM 핸들러 등록: cancel 대신 stop 이벤트 set → 새 잡 클레임 중단,
  하던 잡은 마무리 후 자연 종료(트레이스백 없이 exit 0). 신호 재수신 시 강제 종료
- 종료 유예 LPS_SHUTDOWN_GRACE_SEC(기본 60s) 초과 시 강제 취소(잡은 lease 만료 후 재큐)
- 워커/리퍼가 예외로 죽으면 기존처럼 전파하되, finally에서 남은 태스크 취소·완주 대기 후 정리
- 리스너·어댑터 정리를 항목별 try/except로 격리 — 하나 실패해도 나머지 Chrome 정리
- 웜업(bg) 태스크는 종료 신호 즉시 취소해 어댑터 락 해제
- compose lps-worker에 stop_grace_period: 75s (기본 10s면 드레인 전 SIGKILL)
- 운영 가이드에 워커 종료 절차 문서화

검증: SIGTERM/SIGINT 실기동 테스트 — graceful 로그 후 exit 0, 기존 테스트 26개 통과

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 09:03:57 +09:00
민헌
ddb6f6bd2f fix(lps): 워커 로그 노이즈 2건 제거 — 리퍼 네이버 스킵 + 폴백 데드라인 cancel 제거
- browser-reaper가 close_if_idle 없는 어댑터(네이버 httpx)를 건너뛰도록 getattr 가드
  → 30초마다 반복되던 ERROR 로그 제거
- 폴백 데드라인을 wait_for(cancel) → asyncio.wait(버림)으로 변경
  → in-flight page.goto 취소 시 patchright 내부 future가 남기는
    'Future exception was never retrieved' 노이즈 제거, 페이지 어중간 상태 방지
  → 버려진 태스크는 강한 참조 집합(_abandoned_fallbacks)에 보관(GC 중도 파괴 방지),
    종료 시 콜백이 예외 회수 후 집합에서 제거
- 데드라인 테스트에 잔여 태스크 배수(drain) 검증 추가

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 08:50:12 +09:00
민헌
08a9c52088 docs(lps): 아키텍처에 API 멀티코어 스케일·커넥션 풀 자동산정(6-1) 추가
process_count 를 늘릴 때 풀 오버서브스크립션→커넥션 고갈 함정과
connection_budget 기반 자동 역산 해법을 문서화.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 08:35:48 +09:00
민헌
0e30354ba5 feat(lps): 로컬 워커·부하테스트(GUI) 실행 스크립트 추가 (대화형)
기존 run_local_server.sh 스타일에 맞춘 대화형 .sh 두 개.

- run_local_worker.sh: 동시성(WORKER_CONCURRENCY)·Chrome 프로필(LPS_PROFILE_DIR)
  선택 후 워커 기동. 실행 중 워커 감지 시 교체 여부 확인. venv/config 자동 보장.
- run_loadtest_gui.sh: Locust 웹 UI(:8089) 기동. 워커 실행 중이면 경고(크롤 비용),
  API 미기동 시 PROCESS_COUNT 받아 백그라운드 기동(종료 시 함께 정리). 부하생성기 --processes.
- .gitignore(lps): .profiles/(Chrome 쿠키·cf_clearance 세션) 커밋 방지
- README·operations: 새 스크립트 안내로 갱신

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 08:33:11 +09:00
민헌
9c2432c818 feat(lps): process_count 기반 커넥션 풀 자동 산정 — 멀티코어 풀 오버서브스크립션 방지
connection_budget(기본 40)을 두고, 기동 시 process_count 에 맞춰
pool_size/max_overflow 를 역산: (pool+overflow)×2엔진×process_count ≤ budget.
워커를 늘려도 config 가 스스로 예산을 지켜 커넥션 고갈→요청 실패를 예방.

- config_models: MainDBConfig.connection_budget 추가
- server_configs: _autosize_pool(process_count 확정 후 산정) + _apply_pool_env_override
  (우선순위 = 명시 DB_POOL_SIZE > 자동 산정 > toml pool)
- web_main: 기동 로그에 실효 풀/총커넥션/예산 출력
- env: DB_CONNECTION_BUDGET override, docker-compose 에 knob 노출
- tests: test_pool_autosize 10케이스(예산 준수·분할비·비활성·infeasible 바닥)
- docs: operations 2-1 멀티코어/풀 섹션 + loadtest README 자동산정 표

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 23:59:05 +09:00
민헌
a63793daf0 test(lps): Locust API 부하 테스트 + 멀티코어 벤치 (풀 병목 발견·증명)
API(enqueue/조회, asyncpg I/O 바운드) 부하 테스트로 멀티코어 활용·한계 측정.

- loadtest/locustfile.py: enqueue(고유코드 write)+조회 가중 부하.
- loadtest/bench_multicore.sh: PROCESS_COUNT 1→N 자동 비교(--processes 로 부하생성기도 멀티프로세스).
- server_configs: PROCESS_COUNT / DB_POOL_SIZE / DB_MAX_OVERFLOW env override(코드·toml 수정 없이 튜닝).
- loadtest/README.md: 발견 문서화.

발견(11코어·1500users): 기본 풀(10/20)로 워커 늘리면 실패 폭증(1w=0 → 4w=10336) —
(pool+overflow)×2엔진×workers=240 > PG max_connections=100 커넥션 고갈(SQLAlchemy pool checkout 실패).
증명: 풀 8/4(96<100)로 PC=4 재실행 → RPS 1336→2705(2배), 실패 0. 코드는 멀티코어 활용 가능,
막는 건 풀 오버서브스크립션. 규칙: (pool+overflow)×2×process_count ≤ max_connections.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 23:45:25 +09:00
민헌
eba78dbbb9 docs(lps): README 최신화 — Dockerfile.worker·문서목록(배포/관측/크롤러논의)
폴더구조에 Dockerfile.worker, 문서 표에 Docker배포·관측·decision-openmarket-crawler 반영.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 23:32:52 +09:00
민헌
97e566f844 docs(lps): 운영 가이드 — 관측(readyz/ops/알림)·라이브 스모크·Docker 배포
P4/P5 반영: /readyz·/v1/lps/ops·워커 하트비트·임계 알림 env, LPS_LIVE 라이브 스모크,
docker compose 배포(헤드풀 Chromium+Xvfb·시크릿 env·프로필 볼륨·HEALTHCHECK).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 23:27:02 +09:00
민헌
eb1ff8fee1 test(lps): P5 브라우저 경로 스모크 — search 오케스트레이션 + 라이브 드리프트 감지
가장 취약·복잡한 경로(회전·재시도·차단감지)의 자동 테스트 공백을 메운다.

- mock 하니스(_MockPage/_MockCtx/_MockProxy)로 search() 를 결정론 테스트(브라우저 없이):
  정상 반환 / 차단→IP회전→복구 / 프록시전송오류→회전→복구 / 비프록시오류→실패(무회전) /
  차단 소진→blocked 실패. 5종.
- 라이브 스모크(각 어댑터 실제 사이트 검색→파싱): 셀렉터·안티봇 드리프트 감지. IP 의존·느려서
  기본 skip, LPS_LIVE=1 로 명시 실행(수동/야간). 네이버 스모크 통과 확인.

전체 96 passed, 5 skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 23:25:42 +09:00
민헌
d6dd47e652 feat(lps): P4 관측·알림·워커 헬스 — readyz/ops + 하트비트/HEALTHCHECK + 임계 알림
프로덕션 운영 가시성. 행/좀비 워커 감지 + 큐/차단 지표 노출 + 임계 알림.

- API: /readyz(DB 도달성=readiness, 실패 503; /healthz=liveness와 구분).
  /v1/lps/ops(플랫 JSON): 큐 카운트 + oldest_pending_sec(큐지연) + dead_1h + stuck_running + blocks_1h.
- crud: JobQueue.ops()/ping(), BotDetectionLog.recent_count().
- worker: run_ops_monitor — 하트비트 파일 주기 갱신(Docker HEALTHCHECK 가 신선도로 행 워커 감지)
  + 임계(DEAD/차단/큐지연/stuck) 초과 시 WARN 로그 + (LPS_ALERT_WEBHOOK 있으면) Slack 호환 웹훅.
- Dockerfile.worker: HEALTHCHECK(하트비트 <120s). 임계·웹훅은 env(LPS_ALERT_*).
- 테스트: readyz/ops 2종.

검증: 컨테이너 healthy 판정, 하트비트 갱신, ops 스냅샷 정상. 91 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 23:21:43 +09:00
민헌
3a8d669970 feat(lps): P2 시크릿 env 주입 + Chrome 프로필 영속 볼륨
프로덕션에서 시크릿을 이미지에 굽지 않고 env 로 주입, 재시작 시 챌린지 쿠키 유지.

- server_configs: 시크릿 env override 추가(house 의 DB override 패턴 확장) —
  OPENAI_API_KEY/MODEL, DECODO_HOST/USERNAME/PASSWORD/COST_PER_GB, NAVER_KEYS="id:sec,...".
  로컬은 env 미설정 → config.local.toml 그대로.
- worker_main: Chrome 프로필 경로를 LPS_PROFILE_DIR(기본 /tmp)로 env 화 → 영속 볼륨 마운트 가능.
- docker-compose: lps-worker 에 lps-profiles 볼륨(/profiles) + 시크릿 env 주입 주석 예시.
- config.example: 프로덕션 env 주입 목록 문서화.

검증: env override 동작(OpenAI/DECODO/Naver), 프로필이 /profiles 볼륨에 Cookies 포함 저장(cf_clearance 유지). 89 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 23:04:37 +09:00
민헌
94d4e54d5e feat(lps): P1 컨테이너화 — 워커 이미지(Chromium+Xvfb) + compose
프로덕션 배포 토대. headless Chrome 은 Akamai/Cloudflare Turnstile 에 탐지됨(실측 실패)
→ 컨테이너에선 Xvfb(가상 디스플레이)로 headful Chromium 을 실행한다.

- Dockerfile.worker: chromium+xvfb+xauth+한글폰트. Xvfb 를 백그라운드로 띄우고 python 을
  exec 승계(로그가 docker logs 로 나옴; xvfb-run 은 stdout 을 삼킴). root=--no-sandbox 필수.
- Dockerfile: API 전용(lean, 브라우저 불필요)임을 명시.
- docker-compose: lps-api(:9600) + lps-worker(shm 1g, DB=host.docker.internal) 서비스 추가.
- browser_base: 브라우저 실행 대상 env 화(LPS_CHROME_EXECUTABLE/CHANNEL). 컨테이너=시스템
  chromium(+no-sandbox/disable-dev-shm-usage), 로컬=실제 Chrome(channel=chrome).

검증(컨테이너): DB연결·프리플라이트·쿠팡(Akamai)·gmarket(Turnstile) 웜업 통과 →
end-to-end 검색 DONE(8몰: 네이버·쿠팡·G마켓·옥션·11번가, 비용 $0.016). docker logs 로 로그 확인.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 22:56:56 +09:00
민헌
e2cd2570e3 docs(lps): 오픈마켓 크롤러 유지 여부 논의 문서
팀 논의용 의사결정 메모 — 크롤러가 비용·복잡성 ~80%인데 ROI 미증명.
현황 데이터(비용 87% DECODO·최저가 이긴 적 0·플래키), 핵심 질문(by_mall 소비처),
옵션 4종(A.TTL캐시 / B.게이트 / C.gmarket드롭 / D.현행), curl_cffi 불가 근거.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 21:51:21 +09:00
민헌
10eb67e8c6 docs(lps): 문서 최신화 — 오픈마켓 폴백·Turnstile·비용계측·동시성·프록시복원력
이번 라운드 변경을 README/아키텍처/운영 가이드에 반영.
- README: 기능표(오픈마켓 폴백·원가계측·다중병렬), 워크플로우 4-1 폴백 단계, 폴더구조
- architecture: 안티봇 대응(쿠팡 Akamai / ESM Cloudflare Turnstile 웜업·동적차단),
  검색 원가 계측(CDP 실측·컴포넌트별 비용), 다중 상품 병렬, 유휴 브라우저 정리
- operations: 로그 항목(프리플라이트·웜업·IP회전·데드라인·유휴정리), 문제해결(407·Turnstile·
  Chrome 정리·느림/비쌈), 배포유의, 원가/몰별 DB 쿼리

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 21:18:12 +09:00
민헌
77a36c43d4 fix(lps): gmarket 웜업 신뢰성 — 나쁜 IP 시 회전 재시도
웜업이 인터랙티브 Turnstile(IP 평판 나쁨)로 실패하면 다른 IP 로 회전해 최대 3회 재시도.
gmarket 커버리지 플래키함 완화(100 IP 풀에서 좋은 IP 를 찾을 확률↑).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 21:06:29 +09:00