Commit Graph

400 Commits

Author SHA1 Message Date
민헌
479cb862b1 feat(lps-admin): 상품·가격 — '몰별 최저가'+'검증 링크' 를 몰별 가격 비교로 통합
두 카드가 사실 같은 데이터(by_mall)를 다르게 그린 것이라 하나로 합침.
상위 N개(3/5/10/전체 선택)를 순위·쇼핑몰·상품명·가격·최저가 대비 델타·
링크 한 줄로 보여주고 가격 비교 막대를 얹어 한눈에 비교 가능.

- MallEntry 타입 필드명 버그 수정(mall/url → mall_name/detail_url/name)
  — 기존 몰별 최저가 차트에 몰 이름이 안 보이던 원인. 이제 표시됨.
- 상위 N 은 프리셋 칩(by_mall 이 몰별 dedup 소집합이라 자유입력보다 적합).
- '검색어'는 항목별로 없어 각 몰의 상품명(title)으로 대체 — 같은 상품
  검증 역할.
- 모바일 가로 스크롤 해소: 상단 네비 overflow-x-auto 가 페이지를 늘리던
  것(루트 w-full 누락)과 master-detail 그리드 자식 min-w-0 누락 동반 수정.
- 실기동 검증: G마켓 56,880/플랜88 59,900 렌더·칩 토글·데스크톱/모바일
  hscroll 0·콘솔 에러 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 10:57:07 +09:00
민헌
a306ad4cf3 docs(lps-admin): README 에 테스트 검색 페이지 추가
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 09:14:14 +09:00
민헌
c126d56631 feat(lps-admin): 테스트 검색 페이지 — 임의 상품을 실제 파이프라인에 흘려 점검
셀렉터·프록시·AI 매칭 회귀를 UI 에서 즉석 점검. API 는 lean(브라우저
없음)이라 크롤은 워커에만 있으므로, 기존 POST /v1/lps/search 로 잡을
넣고 GET /jobs/{id} 를 2초 폴링해 결과를 시각화(신규 백엔드 0).

- 폼(상품명 필수+모델/규격/제조사/현재가) → 제출 → 진행 스텝퍼
  (대기→크롤·판정중→완료, 1초 티커 경과시간) → 결과.
- 결과: 판정(최저가·소스), 파이프라인 퍼널(수집→필터→이상치→AI→top-N
  단계별 잔존 수), 소스별 수집(+실패 시 error 표시), 검색 원가
  (AI/프록시 분해·전송량), 매칭 상품 표, 원본 JSON.
- product_code=TEST-<시각> 자동 — 프로덕션과 필터 구분(negodata 는
  UUID 만 매핑, 연동 무영향). 자격증명은 서버에만, 결과엔 집계 사용량만.
- 워커 미실행 시 8초 후 '워커 확인' 힌트, DEAD 시 오류 표시.
- 작업 큐에 '테스트만'(TEST- 접두) 필터 칩 추가.
- 실기동 검증: 맥심 커피 검색 → 퍼널 80→2·쿠팡 9.5s·원가 /bin/zsh.002 렌더,
  콘솔 에러 0, 모바일 hscroll 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 09:13:54 +09:00
민헌
3c8d0af2e4 chore: .gitignore 에 .gstack/ 추가 (gstack 로컬 상태 디렉터리)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 22:59:14 +09:00
민헌
74fce19be9 style(design): FINDING-009 — 비용 빈 상태에 다음 행동 안내 추가
'없습니다'로 끝나던 빈 상태에 '워커를 켜고 검색을 돌리면 채워집니다'
안내를 붙여 사용자가 다음 행동을 알 수 있게.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 22:58:51 +09:00
민헌
1006ad11ea style(design): FINDING-012 — 접근성 통일: 검색 라벨·행 키보드 선택·이모지 숨김 [subagent]
Settings 는 label 연결이 모범인데 Jobs/Products 검색 인풋만 접근
가능한 이름이 없었고, Products 목록(button)과 달리 Jobs 행(tr onClick)
은 키보드로 선택 불가였다. aria-label 2곳, tr tabIndex+Enter/Space
핸들러+focus-visible, 장식 이모지(⚠✓🎉) aria-hidden 처리.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 22:58:51 +09:00
민헌
645a0362b0 style(design): FINDING-011 — 타이포 스케일 정수 6단으로 통일 (본문 14px) [subagent]
작은 앱에 0.5px 간격 10종(10.5~22)이 혼재 — 12 vs 12.5 는 위계를
만들지 못하고 half-pixel 은 zoom/서브픽셀에서 반올림이 갈린다.
{11,12,13,14,17,22} 정수 6단으로 정규화(순서 보존 매핑), 본문
13.5→14px(FINDING-004). 빌드·렌더 검증.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 22:58:14 +09:00
민헌
29e54ad838 style(design): FINDING-007 — 고정 150px 파이의 ResponsiveContainer 제거
recharts 가 '고정 폭·높이엔 ResponsiveContainer 불필요' 경고를 콘솔에
반복 출력(페이지당 4회). PieChart 직접 렌더로 전환.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 22:54:46 +09:00
민헌
10ea8ddd45 style(design): FINDING-010 — Crawl.tsx 하드코딩 hex 를 @theme 토큰으로 단일화 [subagent]
Crawl 이 dead-600/ink-400/cost-ai 와 동일한 값을 raw hex 로 재복제해
토큰 변경 시 어긋나는 이중 정의 상태였다. 종료사유 팔레트를 상태
토큰(ok/run/warn/dead)+신규 neutral-300/400 으로 치환, ReferenceLine·
막대·폴백 hex 전부 var() 참조로. src 내 raw hex 0 확인.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 22:53:58 +09:00
민헌
433b973505 style(design): FINDING-003 — ink-400/500 캡션 대비 WCAG AA 통과로 다크닝
ink-400(#9aa1ac)이 11~13px 소형 텍스트(타일 부제·테이블 헤더·각주)에
쓰이는데 대비 2.54:1 로 AA(4.5:1) 미달. ink-400→#6d7480(4.59:1),
ink-500→#5d6470(5.81:1)으로 2단 위계 유지한 채 통과. 실측 재검증.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 22:52:37 +09:00
민헌
a750c1098a style(design): FINDING-001 — 모바일(<md) 사이드바를 상단 네비로 전환
375px에서 사이드바가 196px을 고정 점유해 콘텐츠가 낱글자로 세로
분해되고 /jobs 에 가로 스크롤(484px)이 생겼다. <md 에서 상단 바
+가로 스크롤 네비(터치 44px, FINDING-006)로 전환, 로고 그라디언트
hex 를 primary-400 토큰으로 승격. 검증: dashboard/jobs 375px
hscroll=false, 스크린샷 확인.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 22:51:53 +09:00
민헌
5e286d7949 chore(lps-admin): package-lock name 동기화 — 폴더명 변경 후속
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 22:39:49 +09:00
민헌
992a3246a8 chore(lps-admin): 폴더명 lps-temp-fe → lps-admin
임시 FE 가 정식 관리자 콘솔이 되어 이름을 용도에 맞게 변경.
루트 병렬 유지(lps/ 는 파이썬 백엔드 루트라 중첩 시 비대칭 +
Docker 빌드 컨텍스트 오염). package.json name 동기화.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 22:07:30 +09:00
민헌
6eb71eb035 feat(lps-temp-fe): React 관리자 페이지 — 대시보드·작업큐·상품가격·크롤·비용
임시 단일 HTML 콘솔을 걷어내고 5페이지 관리자 앱으로 교체(협의:
모니터링+필수 액션, negodata 계열 경량 스택 — Vite+React+TS+Tailwind
+recharts+react-query+react-router).

- 대시보드: /ops 5초 폴링 스탯 타일 8종 + 임계 배너(서버 AlertConfig
  기본값 미러) + 세션 내 누적 큐 추이.
- 작업 큐: 상태 탭·상품 검색·페이지네이션·상세 패널(결과/원가/오류),
  DEAD 재큐 버튼.
- 상품·가격: 가격 추이 3선 + 몰별 최저가 바 + 검증 링크 + 수동 재검색.
  시리즈 색 엔터티 고정 — 네이버 #008a43 은 쿠팡 적색과의 적록색약
  분리(ΔE 14.5)·3:1 대비를 검증해 톤다운한 값.
- 크롤 상태: 종료사유 도넛(의미 기반 상태색)·세션당 요청 수 분포
  +차단 시작 기준선(예산 튜닝 뷰)·차단 추이/이력·세션 테이블.
- 비용: 시간별 원가 스택(AI vs 프록시)·건당 평균·평균 소요(축이 달라
  별도 차트 — 이중축 금지).
- 설정: API 주소·guard 키(localStorage → X-API-Key 자동 첨부).
  서버 설정 변경 UI 없음(toml 단일 소스 원칙).
- dev 는 vite 프록시(:9600)로 CORS 불필요. 5페이지 실렌더 스크린샷 검증.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 21:59:15 +09:00
민헌
61b438d8d0 feat(lps): 관리자 FE 용 API 6종 — 잡 목록/재큐·상품 목록·IP세션/차단/비용 통계
React 관리자 페이지(협의: 모니터링+필수 액션)의 데이터 소스.

- GET /v1/lps/jobs: 최신순 목록+총건수, status/q(상품코드·명) 필터.
  결과에 outcome·최저가·검색원가·오류를 평탄화해 목록에서 바로 보이게.
- POST /v1/lps/jobs/{id}/requeue: DEAD 재큐(attempts 리셋+pg_notify 워커
  깨움). 활성 중복(dedupe)이면 DB_ALREADY_SAME_KEY 로 거절.
- GET /v1/lps/products: 상품별 최신 스냅샷+누적 검색 수(최근 검색순).
- GET /v1/lps/stats/ip-sessions: 종료사유 분포·요청수 히스토그램·차단
  세션 최소 요청수(예산 튜닝 기준선)·최근 세션 50.
- GET /v1/lps/stats/bot: 시간대별 차단 + 최근 감지 목록.
- GET /v1/lps/stats/cost: 시간별 원가(AI/프록시 분해)+평균 소요.
- AdminService/admin_protocol/admin 라우터 신설, guard 일괄 적용.
  설정 변경 UI 는 두지 않음 — toml 단일 소스 원칙.
- 테스트 9건 추가, 전체 154 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 21:42:38 +09:00
민헌
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