몰별 성공/실패 표시
lps_db.price_history 에는 몰별 정보가 다 있는데(naver_lowest·coupang_lowest·by_mall),
동기화가 final_source(이긴 몰) 하나만 남기고 나머지를 버려서 API 로는
"네이버는 어땠는지"를 알 수 없었다. 실제로 네이버는 빈손이고 쿠팡만 성공하는
케이스가 기본값처럼 나오는 중이라 화면에 드러낼 필요가 있다.
→ price_history.by_mall(JSONB)을 partner.item_internet_lowest_prices 로 그대로
미러링한다(열린 스키마 — 오픈마켓 폴백이 늘어도 스키마 변경 불필요).
- crud: 읽기 계약에 by_mall 추가 + SELECT 포함
- model: item_internet_lowest_prices.by_mall
- service: 언팩·저장
- protocol: LowestPriceEntry.by_mall
- init.sql: 테이블 정의 + 하단 보정 ALTER(기존 DB 반영용)
강제 재검색
POST /v1/item/{id}/lowest-price?force=true → LPS 네거티브 캐시 우회.
사용자가 '다시 검색'을 누른 경우에만 true.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1) 캐시 히트가 price_history 를 남기지 않던 문제
not_found 는 24시간 네거티브 캐시에 들어가는데, 캐시에 걸린 조기 반환 경로만
_record_history 를 호출하지 않았다(다른 모든 경로는 호출).
그 결과 잡은 완료인데 price_history 에 새 행이 없어, 이를 폴링하는 소비자
(negodata 최저가 모달)가 결과를 영영 못 받고 로딩만 돌았다.
→ 캐시 히트도 '이 잡의 결과'이므로 이력을 남긴다.
2) force 플래그
negodata 는 이제 수동 트리거 전용인데 is_negative() 가 job_type 을 보지 않아
사람이 직접 누른 재검색까지 캐시가 가로막았다. 게다가 캐시 키가 product_code 라
상품명·모델을 고쳐 재시도해도 동일하게 막힌다.
→ SearchItem.force=true 면 NegativeCache.drop() 으로 기록을 지우고 실제 검색.
기본 요청은 캐시를 그대로 써서 비용 절감 효과는 유지.
실측: 캐시에 막혀 not_found 만 반복하던 상품이 force 재검색에서 2라운드 만에 found(8,500원).
⚠️ protocol.py 변경은 lps-api 와 lps-worker 를 함께 재빌드해야 반영된다
(API 만 옛 스키마면 pydantic 이 force 를 조용히 버린다 — 실측으로 확인).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
google-chrome-stable(Linux)은 amd64 전용이라 워커 이미지는 본질적으로 amd64다.
arm64 맥에서 platform 없이 빌드하면 arm64 로 시도하다 Chrome 의존성
(libasound2·libxfixes3 등)에서 exit 100 으로 실패하고, 그 결과 예전에 빌드된
구 chromium 이미지로 뜨면서 LPS_CHROME_EXECUTABLE(/opt/google/chrome/chrome)
경로가 없어 쿠팡 웜업이 매번 실패했다.
platform 명시 후 Rosetta 에뮬로 실제 Chrome 150 이 뜨고 쿠팡 Akamai 챌린지를
시도 1회에 통과함을 확인. prod(amd64 서버)에서는 no-op.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
출처: '260716_AIO2O 수정 및 요청사항_v2_memo check.xlsx' > '협상카드 수정' 시트(D열).
- 스크립트 개정: NGC-003(장기 관계 강조)·NGC-004(명분 제공)·NGC-007(예산 상한 안내)·NGC-008(시장가 근거 제시)
- 미사용('IMK 거래 특성과 관련성 부족') 카드 삭제: NGC-006(총비용 가치 설명)·NGC-009(조건부 가격 조정)
- init-data.sql(신규/초기화 설치 정본)에서 006·009 INSERT 제거 + 4개 멘트 갱신
- 동일 최종본을 기존 DB 보정용 alter(postgres-init/alters/2026-07-24-nego-card-scripts.sql)로도 제공(멱등)
검증: 시드 nego_cards VALUES 를 temp 테이블에 강제 평가 → 9행·전부 유효 jsonb. seed=alter=로컬 DB 일치.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1) 표시가≠투찰가 (배포 서버 협상 결과 오류)
- 카운터 제시 중 멘트의 절충/중간 변수(middle_price·target_mid_price)가 vars_for 재계산으로
compute_counter 의 target 클램프·prev_customer 갱신과 어긋나, 화면엔 1,740,000 인데 실제로는
1,700,000 으로 타결되던 문제 → vars_for 에서 세 변수(counter/middle/target_mid)를 pending 으로 고정
- WC-03 최후통첩 멘트가 제시 금액을 안 보여줘 수락 시 화면에 없던 target 으로 타결되던 문제 →
멘트에 {target_price} 명시(seed init-data.sql). 운영 DB 는 별도 UPDATE 필요
2) 카드 사용 횟수 3회 초과
- quotation_settings.card_count(협상카드 사용 횟수 상한, 기본 3)가 어디서도 강제되지 않던 죽은 설정 →
에이전트가 sessions.qt_setting_id 로 card_count 를 조회해 협상카드 재생 수를 min(선택수, card_count)로 캡
(session.action_space_size 는 소진 판정 전용 — Q-table 은 카탈로그 전체로 별도 고정)
- 회귀 테스트: 표시가==타결가(test_card_tactics·test_p7_chat), card_count 로드(test_context_loader). 전체 153 pass
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- agent: 인터넷 최저가(items.internet_lowest_price)를 협상 파이프라인에 연결
(nego_context_crud 조회 → context loader → session.context → chat_engine.vars_for).
값 있을 때만 {internet_lowest_price}/internet_min_price 치환, 미수집(0)이면 원형 유지(허위 시장가 방지)
- negodata: 카드 에디터 변수 카탈로그에 '인터넷 최저가' 추가
- negodata: 견적 생성 카드 선택 게이팅 — 조건전략 미작성({customer_condition} 잔존)·
최저가 미수집 상품의 {internet_lowest_price} 인용 카드는 선택 불가(상품 변경 시 자동 해제 포함)
- negodata: 상품 엑셀 업로드의 협력사 목록 size 1000→100
(PageParams le=100 초과 시 422 → 목록 비어 전 행 '미등록 협력사' 오탐)
- seed: WC-01 의 데이터 소스 없는 {customer_reference} 제거 → 고정 문구로 대체
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- UserRole 1=일반 2=최고관리자 3=개발자(내부 운영). 개발자 계정은 회원 목록·총계에서 제외해 고객사에 노출하지 않음
- 계정 생성 시 권한 선택(일반/최고관리자) 추가, 개발자는 앱에서 부여 불가(DB 시드 전용)
- 사이드바 '개발자' 그룹 신설 — 회사 설정·디자인 시스템을 개발자에게만 노출
- /dev/design 디자인 시스템 페이지: 색 토큰·타이포·버튼·배지·입력·반경, 목록 화면 구조, 반응형 기준, URL 상태 규칙, 회사 커스터마이징 훅
- 공급사 재협상 요청(#15) 플로우 문서 — sessions.custom 기반(DDL 0)
- 채팅 링크로 바로 진입하면 세션이 협상생성(1)에 머물러 오프닝 메시지가 seed 되지 않던 문제:
chat init/messages 에서 participate 와 동일하게 협상중(2)으로 전이(마감 견적·마감시간 초과는 제외)
- 부가정보 저장이 1301 로 실패하던 문제: '협상완료' 요약은 chat_end=false 라 세션이 아직 협상중이므로,
마지막 말풍선이 summaryRSP/CM 이면 협상중 세션도 저장 허용
- 채팅 언마운트 시 messages 캐시도 제거 — staleTime:Infinity 라 빈 목록이 남아 재진입해도 빈 화면이던 문제
- 채팅 스크롤을 컨테이너 기준 scrollTo 로 변경(rAF 후 실행) — 요약·폼이 뒤늦게 레이아웃되면 중간에 멈추던 문제
- 모바일 채팅 상단 여백 제거(pt-[64px] 는 우측 협상절차 카드가 있는 lg 이상만)
- 노치 대응: viewport-fit=cover + safe-t/b/x 유틸 신설, 헤더·하단 액션 덱·드로어·랜딩 헤더에 적용, h-screen→100dvh
- 회사 설정 hidden_fields 신설: 상품 목록·등록폼·엑셀 양식에서 감추고 목표가 후보에서도 제외
- 용어 카탈로그 9→37개(상품·협력사·견적 그룹), 목록·폼·엑셀 헤더 배선
- 엑셀 '최저한도' → '인터넷 최저가' 표기 정정(구양식 헤더는 alias 유지), 공급사 컬럼 검증 유지
- 목표가 산정내역에 적용 모드·숨김 필드 반영, 목표가 상한 라벨에 산식(상품단가×2) 표기
- 공급사 포털: 초청 링크 session_id 로 로그인 전 회사 브랜딩 조회(무인증 엔드포인트), 기본 브랜드 negotium 워드마크, negotium·AI O2O 크레딧 표기
- 목록 테이블 뷰포트 기준 전환·툴바 줄바꿈·협상현황 카드뷰로 모바일/태블릿 대응
- 이용안내 FAQ 탭 추가, 이용안내·회사설정 탭 URL 쿼리 동기화 및 빠른이동 등록
- 협상카드 성공률(사용 세션 대비 타결) 집계 추가, 견적 생성 카드선택 순위 정렬·통계 TOP5
- 상품 엑셀: 품목코드·모델명 라벨화, 리드타임 suffix 제거, 공급사 컬럼 신설(등록 후 supplier_items 매핑)
- 설정 화면 JSON 불러오기/내보내기(빈 값은 미갱신), IMK 설정 JSON 보관
- 설정·회원 페이지 새로고침 forbidden 수정(자식 loader 가 initAuth 대기)
- 협력사·견적 목록도 포털형 카드(툴바+테이블 결합)로 통일
- 협상카드 성공률(사용 세션 대비 타결) 집계 추가, 견적 생성 카드선택 순위 정렬·통계 TOP5
- 상품 엑셀: 품목코드·모델명 라벨화, 리드타임 suffix 제거, 공급사 컬럼 신설(등록 후 supplier_items 매핑)
- 설정 화면 JSON 불러오기/내보내기(빈 값은 미갱신), IMK 설정 JSON 보관
- 설정·회원 페이지 새로고침 forbidden 수정(자식 loader 가 initAuth 대기)
- 협력사·견적 목록도 포털형 카드(툴바+테이블 결합)로 통일