Go to file
Haewon Kam 77d96cd4a4 [fix] landing: 히어로 CTA 문구 — 클릭 후 일어나는 일에 맞춘다
여기서 직접 체험  →  협상 예시 체험
  데모 요청        →  실제 데모 받기

두 버튼은 성격이 전혀 다른데 문구가 그걸 구분하지 못했다.
1차는 페이지 안에서 바로 조작하는 인터랙티브 데모고, 2차는 이름·이메일을 받는
리드 폼이다. 클릭 직후 경험이 다르므로 문구도 갈라야 한다.

협상 예시 체험
  고정 데이터 시뮬레이션이라 "예시"를 밝힌다. 다만 "보기"로 낮추지는 않았다 —
  도착 섹션이 "직접 경험해 보세요" 이고 역할 카드가 "공급사로 해보기" 라,
  버튼만 구경하는 말이면 클릭 전 기대와 도착 후 요구가 어긋난다.

실제 데모 받기
  누르면 폼이 뜨고 데모는 이메일로 온다. "직접 체험" 이라 쓰면 눌렀을 때 바로
  만질 줄 알았다가 입력을 요구받아 낚인 느낌이 된다. "받기" 는 이메일로 온다는
  사실이 드러나서 폼이 뜨는 게 자연스럽다.

모달 제목도 "데모 요청" → "실제 데모 받기" 로 맞췄다. 버튼이 "받기" 인데 열린 창이
"요청" 이면 전환 순간에 다른 화면처럼 읽힌다.

제출 버튼은 "데모 신청" 그대로 둔다. 같은 원칙을 적용하면 그 클릭으로 실제 일어나는
일은 신청 접수이지 데모 수령이 아니다(수령은 이메일로 나중에).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 09:47:07 +09:00
agent [feat] negodata·agent: 목표가 네고 적용/미적용 산정근거·와일드카드 1% 10원 반올림·표 xl 전환·QA 체크리스트 2026-07-31 10:22:51 +09:00
backend [feat] negosium: 협상 챗 헤더 정리 — 상품명 대표 + 견적번호 2026-07-30 08:31:16 +09:00
docs [feat] negodata·agent: 목표가 네고 적용/미적용 산정근거·와일드카드 1% 10원 반올림·표 xl 전환·QA 체크리스트 2026-07-31 10:22:51 +09:00
frontend [feat] negodata·negosium: 엑셀 데이터 내보내기·견적상세 협상카드 연동·드로어 뒤로가기 2026-07-30 11:03:51 +09:00
landing [fix] landing: 히어로 CTA 문구 — 클릭 후 일어나는 일에 맞춘다 2026-08-07 09:47:07 +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: 목표가 네고 적용/미적용 산정근거·와일드카드 1% 10원 반올림·표 xl 전환·QA 체크리스트 2026-07-31 10:22:51 +09:00
postgres-init [feat] negodata/backend: 몰별 최저가(by_mall) 노출 + 강제 재검색(force) 연동 2026-07-28 11:55:38 +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