Go to file
Mina Choi 71ef28110b [feat] negodata·agent·negosium: 협상 기준가 회사별 선택(공급가/매입가) + 공급사 포털 안내 회사설정화
어느 가격이 "공급사에 지불하는 단가"인지는 회사마다 달라, 컬럼을 합치는 대신 회사가 고르게 했다.
판정은 settings.features.nego_baseline_field 1순위, 미설정 회사는 price 만 숨겼으면 purchase_price 폴백.
판정식 정본은 negodata/backend/common/nego_baseline.py (agent·negosium backend 가 같은 규칙 미러).

- negodata front: 회사 설정에 협상 기준가 라디오(선택지마다 실제 나갈 문장 미리보기 + 학습 경고),
  '공급사 포털 안내' 탭 신설(협상 유의사항·헬프데스크 연락처), 용어 카탈로그에 target_price·supplier 추가.
  인터넷 최저가는 숨김 대상에서 제외(신규 견적의 유일한 목표가 후보).
  상품 등록 기본값에서 개발용 더미 제거(price 1,000,000·PROD-BAT-###·800,000·대한민국·10 EA·14).
- agent: get_item_price → get_item_baseline (기준가·호칭·회사 용어사전을 한 쿼리로),
  "기존 공급가 대비" 하드코딩을 회사 용어로 치환 + 받침 기준 조사 자동 보정,
  협상 스크립트 4종의 협력사·목표가·배송형태 용어를 {label_*} 토큰화, input_options 도 변수 치환 적용.
- negosium: 협상 화면 기준 단가·배송형태 라벨을 회사 설정 기준으로, 유의사항 본문과 헬프데스크 연락처를
  하드코딩에서 회사 설정으로(미등록 시 영역 숨김). 유의사항의 VAT·배송비 문구 삭제(IMK 0803 ⑥).
- negodata backend: LPS 검색 가격 힌트를 items.price 고정에서 기준가 규칙으로.

검증: 기준가 설정 3 × 숨김 4 = 12조합에서 협상 멘트·포털 표시·LPS 힌트가 전부 일치.
agent 테스트 176건 통과. 보고서 negodata/docs/nego-baseline-verification.md
2026-08-05 11:47:06 +09:00
agent [feat] negodata·agent·negosium: 협상 기준가 회사별 선택(공급가/매입가) + 공급사 포털 안내 회사설정화 2026-08-05 11:47:06 +09:00
backend [feat] negodata·agent·negosium: 협상 기준가 회사별 선택(공급가/매입가) + 공급사 포털 안내 회사설정화 2026-08-05 11:47:06 +09:00
docs [feat] negodata·agent: 목표가 네고 적용/미적용 산정근거·와일드카드 1% 10원 반올림·표 xl 전환·QA 체크리스트 2026-07-31 10:22:51 +09:00
frontend [feat] negodata·agent·negosium: 협상 기준가 회사별 선택(공급가/매입가) + 공급사 포털 안내 회사설정화 2026-08-05 11:47:06 +09:00
landing [chore] landing: prod Dockerfile(SSG 빌드→nginx) 추가 2026-07-24 14:52:16 +09:00
lps [chore] lps: 배포 설정 문서화 + 죽은 env 제거 + 알림 문구 정정 2026-07-28 16:39:14 +09:00
lps-admin feat(lps): docker-compose 에 lps-admin 추가 + 환경설정 config.local.toml 단일화 2026-07-16 11:54:32 +09:00
negodata [feat] negodata·agent·negosium: 협상 기준가 회사별 선택(공급가/매입가) + 공급사 포털 안내 회사설정화 2026-08-05 11:47:06 +09:00
postgres-init [feat] negodata: 타결 상한(done_ceiling) — 세팅 기본율%·견적 override·세션 박제, 견적생성 3단계 타결/낙찰 섹션 분리·재생성 상속 2026-08-04 08:56:03 +09:00
schedules/anchoring [fix] anchoring: 앵커링가 1원 단위 내림 → 10원 단위 반올림 (자동 제안가 정돈, IMK #11 케이스) 2026-07-29 09:51:26 +09:00
scripts [feat] negodata·공급사포털: 상품 필드 숨김 설정·용어 카탈로그 확장·로그인 전 브랜딩·모바일 대응 2026-07-23 11:29:35 +09:00
.gitignore Merge commit 'dc8c288f' — lps-admin 신설·LPS 운영기능(ip_session/alerts/admin API)·최저가 출처링크 반영 2026-07-24 15:30:36 +09:00
docker-compose.prod.landing.yml [chore] 작업분 일괄: logs.sh 컨테이너 메뉴화 · dbeaver 초기화 SQL · landing 운영 compose · negodata/front eslint 도입 · 업로드 샘플 CSV 2026-07-24 18:45:46 +09:00
docker-compose.yml [fix] lps-worker: compose 에 platform=linux/amd64 명시 (arm64 맥에서 이미지 빌드 실패 해소) 2026-07-28 09:16:19 +09:00
logs.sh [chore] 작업분 일괄: logs.sh 컨테이너 메뉴화 · dbeaver 초기화 SQL · landing 운영 compose · negodata/front eslint 도입 · 업로드 샘플 CSV 2026-07-24 18:45:46 +09:00
README.md [docs] readme: 업데이트 + 플로우차트 추가 2026-07-28 09:32:40 +09:00

O2O Negosium

AI 협상 솔루션. 여러 백엔드·프론트·배치가 하나의 PostgreSQL 인스턴스를 공유하고, 전부 docker compose 하나로 뜬다. (인터넷 최저가 검색 LPS 만 별도 DB lps_db 사용.)

서비스 소개

구매기업(바이어)이 협력사(공급사)와 벌이는 가격 협상을 AI 봇이 대신 수행하는 B2B 협상 자동화 솔루션이다. 바이어가 상품·목표가·기간만 정해 견적을 열면, 각 협력사와의 1:1 협상은 강화학습 기반 에이전트가 협상 카드로 밀당하며 진행하고, 마감 시각에 최저 투찰가를 기준으로 자동 낙찰/개찰을 판정한다.

크게 세 축 + 부속으로 나뉜다.

구성요소 역할
바이어 측 negodata (backend + front) 어드민. 상품·협력사 관리, 견적 생성, 마감·낙찰 관리
공급사 측 negosium (backend + frontend) 협력사 포털. 초청받은 협상 챗에 참여해 가격 제시
협상 엔진 agent 실제 AI 협상 봇. 앵커링가·협상 카드로 자동 협상 (강화학습)
부속 lps · anchoring · landing 인터넷 최저가(목표가 재료) · 앵커값 자동 조정 배치 · 솔루션 소개 랜딩

구성

o2o-negosium/
├── docker-compose.yml           # 전체 서비스 (DB 는 compose 밖, config 로 외부 연결)
├── postgres-init/               # DB·스키마·시드 SQL (대상 DB 에 1회 적용)
│
├── backend/                     # negosium 백엔드 — 공급사/협상 API (:9300)
├── frontend/                    # negosium 공급사 프론트 (:3300, 프로덕션 빌드 정적 서빙)
├── agent/                       # 협상 에이전트 — RL(learning 스키마) (:9500)
│
├── negodata/backend/            # negodata 백엔드 — 바이어/어드민 API (:9400)
├── negodata/front/              # negodata 어드민 프론트 (Vite, :3000)
│
├── landing/                     # 솔루션 랜딩페이지 (react-router SSG, :3100)
│
├── lps/                         # 인터넷 최저가 검색: lps-api(:9600) + lps-worker(크롤)
├── lps-admin/                   # LPS 관리자 UI (nginx → lps-api 프록시, :3400)
│
└── schedules/anchoring/         # 앵커링 값 자동 조정 배치 (포트 없음, 상주 스케줄러 + Redis)

negosium·negodata·agent 백엔드는 같은 코드 골격(MVC · 람다 DB · Depends 주입 · JWT 로그인)을 쓴다. 아키텍처/패턴 상세는 각 서브 README 참고:

서비스 / 포트

서비스 주소 역할 DB
negosium-backend http://localhost:9300/docs 공급사·협상 API negosium_db
negosium-front http://localhost:3300 공급사 프론트
agent http://localhost:9500/docs 협상 에이전트(RL) negosium_db (learning)
negodata-backend http://localhost:9400/docs 바이어·어드민 API negosium_db
negodata-front http://localhost:3000 어드민 프론트
landing http://localhost:3100 솔루션 랜딩
lps-api http://localhost:9600/docs 최저가 검색 접수/조회 lps_db
lps-worker 포트 없음 크롤 워커(헤드풀 Chromium) lps_db
lps-admin http://localhost:3400 LPS 관리자 UI
anchoring 포트 없음 앵커링 조정 배치(격주 토 00:00 KST) negosium_db (anchoring)
anchoring-redis 127.0.0.1:6380 anchoring 조회 캐시
autoheal unhealthy 컨테이너 자동 재시작

DB 는 compose 밖 (config 로 연결)

DB 는 docker-compose 에서 관리하지 않는다. 각 backend 는 config.<APP_ENV>.toml 의 접속 정보대로 외부 PostgreSQL(호스트 로컬 postgres, 또는 따로 떠 있는 docker postgres)에 연결한다.

한 PostgreSQL 인스턴스 안에 단일 negosium_db 를 두고 도메인별 schema 로 묶는다. LPS 만 별도 database(lps_db) 를 쓴다.

PostgreSQL (외부, 5432)
├── negosium_db                       ← negosium-backend · negodata-backend · agent · anchoring 공유
│   ├── company / supplier / partner  : 회사·유저·협력사·상품
│   ├── card / quotation / negotiation: 협상 카드·견적·협상 세션
│   ├── learning                      : RL 자산 (agent 소유)
│   └── anchoring                     : 앵커링 조정 (schedules/anchoring 소유)
└── lps_db                            ← lps-api · lps-worker
  • 컨테이너(docker env)에서 호스트 DB 접근: host.docker.internal:5432 (compose 가 DB_HOST 로 override)
  • 로컬 실행/테스트(local·test env): 127.0.0.1:5432 (config.local/test.toml)
  • 계정/database 명은 config 에 맞춘다 (기본 postgres / password).

핵심 플로우

1. 견적 라이프사이클 (전체 개요)

바이어가 견적을 열고 → 협력사가 협상에 참여 → 마감 시각에 판정되는 큰 흐름. 견적 유형은 두 축(재/신규 × 협상 1:1 / 견적 1:N)으로 4종.

flowchart TD
    A["바이어: 상품·협력사·기간 선택<br/>견적 유형 4종 + 낙찰 기준(mid/over_action) 설정"] --> B["목표가·앵커링가 산정<br/>(MD제시가 → 인터넷최저가/매입가/판매가)"]
    B --> C["협력사 초청 (이메일)"]
    C --> D{"견적 유형"}
    D -->|"협상 1:1 (재협상·신규협상)"| E["AI 봇 밀당 협상<br/>(협상 카드 사용)"]
    D -->|"견적 1:N (재견적·신규견적)"| F["정형 흐름<br/>(배송형태·추가할인 확인)"]
    E --> G["세션별 투찰가 확정<br/>(협상완료) 또는 실패"]
    F --> G
    G --> H{"마감 트리거<br/>①마감시각 ②전세션종결 ③수동"}
    H --> I["마감 판정<br/>(최저 투찰가 기준)"]
    I --> J["낙찰 (승자 1)"]
    I --> K["개찰 (낙찰자 미정)"]

2. 1:1 협상 봇 판정 (agent)

협력사가 가격을 제시할 때마다 봇이 앵커링가 기준으로 판정한다. 재제안은 카드를 한 장씩 쓰며 최대 3번, 카드 소진·3번 초과에도 앵커 밑으로 못 내리면 실패(투찰 없음).

flowchart TD
    P["협력사 가격 제시"] --> Q{"제시가 vs 앵커링가"}
    Q -->|"≤ 앵커링가"| R["협상완료 — 투찰 확정"]
    Q -->|"앵커 ~ 앵커×1.02"| S["와일드카드: 1% 인하 요청<br/>(세션당 1회)"]
    Q -->|"앵커×1.02 초과"| T["협상 카드로 재제안"]
    S --> U{"재제안 횟수 ≤ 3?<br/>카드 남음?"}
    T --> U
    U -->|"예"| P
    U -->|"아니오 (소진·3번 초과)"| V["협상 실패 — 낙찰 후보 아님"]

3. 마감 판정

마감 시 협상완료 세션의 최저 투찰가를 본다. 공통 전제: 완료 세션이 없거나(전원 미응찰·협상거부) 동가 최저가 2곳 이상이면 유형과 무관하게 개찰. 그 외 단독 최저가일 때만 낙찰 후보가 되며, 이후 판정이 유형별로 갈린다.

3-1. 견적 1:N — 단독 최저면 무조건 낙찰

가격 구간을 보지 않는다. 생성 시 mid/over_action이 낙찰(AWARD)로 강제되기 때문.

flowchart TD
    M1["마감: 협상완료 세션 최저 투찰가"] --> N1{"완료 세션 있나?"}
    N1 -->|"없음 (전원 미응찰·협상거부)"| O1["개찰"]
    N1 -->|"동가 최저 2곳+"| O1
    N1 -->|"단독 최저"| X1["낙찰 (가격 구간 무관, 무조건)"]

3-2. 협상 1:1 — 가격 구간별, 생성 때 정한 값 적용

앵커링가 이하는 무조건 낙찰. 그 위 구간은 견적 생성 때 미리 정해둔 값(mid_action/over_action, 1=낙찰·2=개찰)을 마감 시 그대로 적용한다. 두 필드는 적용 구간만 다를 뿐 동작은 동일.

flowchart TD
    M2["마감: 협상완료 세션 최저 투찰가"] --> N2{"완료 세션 있나?"}
    N2 -->|"없음 (전원 미응찰·협상거부)"| O2["개찰"]
    N2 -->|"동가 최저 2곳+"| O2
    N2 -->|"단독 최저"| W2{"투찰가 위치"}
    W2 -->|"≤ 앵커링가"| X2["낙찰"]
    W2 -->|"앵커 ~ 목표가"| Y2["생성 시 정한 mid_action 적용<br/>(1=낙찰 / 2=개찰)"]
    W2 -->|"목표가 초과"| Z2["생성 시 정한 over_action 적용<br/>(1=낙찰 / 2=개찰)"]

개찰 = 낙찰자 미정 마감(결렬 아님). 개찰 후 수동 처리로 직접 낙찰 확정(/v1/quotation/award) 또는 재견적 재생성(/v1/quotation/regenerate)이 있다. 비즈니스 로직 정본은 negodata/docs/business-logic.md.

빠른 시작

# 1) DB 준비 (최초 1회) — 사용할 PostgreSQL 에 스키마 + 시드 적용
#    스키마 DDL (구 01~05 통합, 전부 IF NOT EXISTS 라 재실행 안전)
psql -h 127.0.0.1 -p 5432 -U postgres -d negosium_db -f postgres-init/init-data/init.sql
#    로컬/개발 시드 (admin / admin1234, 회사·유저·협상 카드)
psql -h 127.0.0.1 -p 5432 -U postgres -d negosium_db -f postgres-init/init-data/init-data.sql

# (DBeaver 로 처음부터 새로 깔 때는 postgres-init/dbeaver/ 의 0~5 순서 스크립트를 쓴다:
#  0 drop&create → 1 스키마 → 2 시드 → 3 lps_db → 4 카드 리셋 → 5 o2o OWNER 유저)

# 2) 전체 기동
docker compose up -d
docker compose logs -f          # 컨테이너별 로그는 ./logs.sh 메뉴로도 확인
docker compose down

스키마 변경 보정은 postgres-init/alters/ 의 날짜별 SQL 을 대상 DB 에 수동 적용한다 (postgres-init 은 DB 최초 생성 때만 자동 실행되므로, 기존 DB 엔 alter 를 직접 돌려야 새 컬럼이 반영된다).

테스트

# config.test.toml 의 PostgreSQL(기본 127.0.0.1:5432) 이 떠 있어야 한다
cd backend            # 또는 negodata/backend, agent, lps ...
pip install pytest pytest-asyncio httpx
python -m pytest
  • httpx ASGITransport 로 네트워크 없이 앱을 직접 호출하는 e2e.
  • DB_SESSION_MNG 싱글톤의 커넥션 풀이 첫 이벤트 루프에 묶이므로, 모든 테스트가 단일 session 루프를 공유한다(pytest.ini).
  • ⚠️ 테스트는 APP_ENV=test 로 격리한다(테스트 DB). dev DB(negosium_db)에 대고 돌리면 데이터가 날아간다.

성능 / 벤치마크

/loginbcrypt(CPU 바운드) 가 비용의 대부분이다. 초기에는 bcrypt 가 asyncio 이벤트 루프를 막아 아무 일도 안 하는 /healthz 조차 p99 3.3s 가 나왔다.

두 가지 최적화

  1. bcrypt 를 asyncio.to_thread 로 오프로드 — 이벤트 루프 비차단. bcrypt 는 해싱 중 GIL 을 해제하므로 스레드들이 여러 코어에서 실제 병렬 실행된다.
  2. 워커 수 증가 (process_count 1 → 4) — login 처리량을 코어만큼 확장.

Before / After (동일 부하: 100 users)

지표 Before After 변화
/healthz median 1700ms 2ms 850배 개선
/healthz p99 3300ms 14ms 235배 개선
/me p99 3100ms 12ms 258배 개선
/login median 9900ms 220ms 45배 개선
/login p99 15000ms 1400ms 11배 개선
/login RPS 5.5 19.3 3.5배 개선
전체 RPS 17.6 75.2 4.3배 개선

최적화 후 Locust 차트 (100 users)

RPS 가 ~71 로 안정, p95 ~250ms(bcrypt), 실패 0%. median 은 초기 계정생성 버스트 후 바닥으로 떨어진다.

Locust Benchmark

login 은 여전히 가장 느리다(bcrypt 의 의도된 비용). 핵심은 그게 서버 전체를 막지 않는다는 점. 더 높은 처리량은 워커/인스턴스 수평 확장이 정석이다(bcrypt cost 낮추기는 보안 트레이드오프). ⚠️ to_thread 가 이미 단일 워커에서 멀티코어 병렬화를 하므로, 워커를 코어 수만큼 늘리면서 to_thread 까지 쓰면 워커 x 스레드 가 코어를 넘어 오버서브스크립션이 된다(워커는 코어의 절반 안팎).

부하 재현:

docker compose up -d
cd backend && pip install locust
python -m locust -f loadtest/locustfile.py --host http://localhost:9300 --headless -u 100 -r 10 -t 2m
# 부하 중 커넥션 모니터링: psql -h 127.0.0.1 -U postgres -c "SELECT count(*) FROM pg_stat_activity;"

기술 스택

  • 백엔드: FastAPI · SQLAlchemy(async) · asyncpg · PostgreSQL 16 · python-jose(JWT) · bcrypt · uvicorn
  • 프론트: React · react-router v7 · Vite · TanStack Query (negosium-front 는 프로덕션 빌드 정적 서빙)
  • 에이전트: 강화학습(Q-learning, learning 스키마) · OpenAI
  • LPS: 헤드풀 Chromium + Patchright(스텔스 Playwright 포크, 크롤) · autoheal
  • 배치: Redis(anchoring 캐시) · 상주 스케줄러
  • 공통: Docker Compose