From 6534d53696728d8c34280736c7145ec273437e2b Mon Sep 17 00:00:00 2001 From: Mina Choi Date: Tue, 28 Jul 2026 09:30:42 +0900 Subject: [PATCH] =?UTF-8?q?[docs]=20readme:=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20+=20=ED=94=8C=EB=A1=9C=EC=9A=B0=EC=B0=A8=ED=8A=B8?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 190 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 165 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 3a27cab..97a4d06 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,198 @@ # O2O Negosium -동일 구조의 두 서비스(**negosium**, **negodata**)가 **하나의 PostgreSQL 인스턴스**를 공유한다. +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 # 두 backend (DB 는 외부) -├── postgres-init/ # DB·테이블 셋업 SQL (대상 DB 에 1회 적용) -├── backend/ # negosium 백엔드 (포트 9300) -├── negodata/backend/ # negodata 백엔드 (포트 9400) -├── agent/ front/ # (예정) -└── negodata/front/ # (예정) +├── 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) ``` -두 백엔드는 같은 코드 골격(MVC · 람다 DB · Depends 주입 · JWT 로그인)을 쓴다. -아키텍처/패턴 상세는 각 서버 README 참고: [backend](backend/README.md) · [negodata/backend](negodata/backend/README.md) +negosium·negodata·agent 백엔드는 같은 코드 골격(MVC · 람다 DB · Depends 주입 · JWT 로그인)을 쓴다. +아키텍처/패턴 상세는 각 서브 README 참고: +- 백엔드: [backend](backend/README.md) · [negodata/backend](negodata/backend/README.md) · [agent](agent/README.md) · [lps](lps/README.md) +- 프론트: [frontend](frontend/README.md) · [negodata/front](negodata/front/README.md) +- 배치: [schedules/anchoring](schedules/anchoring/README.md) · 관리자 UI: [lps-admin](lps-admin/README.md) + +### 서비스 / 포트 + +| 서비스 | 주소 | 역할 | 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..toml` 의 접속 정보대로 **외부 PostgreSQL**(호스트 로컬 postgres, 또는 따로 떠 있는 docker postgres)에 연결한다. -한 PostgreSQL 안에 서비스별 database 를 둔다. + +한 PostgreSQL 인스턴스 안에 **단일 `negosium_db`** 를 두고 도메인별 **schema** 로 묶는다. +LPS 만 별도 database(`lps_db`) 를 쓴다. + ``` PostgreSQL (외부, 5432) -├── negosium_db ← negosium-backend -└── negodata_db ← negodata-backend +├── 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` (`config.docker.toml`) +- 컨테이너(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`). -| 서비스 | 서버 | docs | database | -|---|---|---|---| -| negosium-backend | http://localhost:9300 | /docs | negosium_db | -| negodata-backend | http://localhost:9400 | /docs | negodata_db | +## 핵심 플로우 + +### 1. 견적 라이프사이클 (전체 개요) + +바이어가 견적을 열고 → 협력사가 협상에 참여 → 마감 시각에 판정되는 큰 흐름. +견적 유형은 두 축(재/신규 × 협상 1:1 / 견적 1:N)으로 4종. + +```mermaid +flowchart TD + A["바이어: 상품·협력사·기간 선택
견적 유형 4종 + 낙찰 기준(mid/over_action) 설정"] --> B["목표가·앵커링가 산정
(MD제시가 → 인터넷최저가/매입가/판매가)"] + B --> C["협력사 초청 (이메일)"] + C --> D{"견적 유형"} + D -->|"협상 1:1 (재협상·신규협상)"| E["AI 봇 밀당 협상
(협상 카드 사용)"] + D -->|"견적 1:N (재견적·신규견적)"| F["정형 흐름
(배송형태·추가할인 확인)"] + E --> G["세션별 투찰가 확정
(협상완료) 또는 실패"] + F --> G + G --> H{"마감 트리거
①마감시각 ②전세션종결 ③수동"} + H --> I["마감 판정
(최저 투찰가 기준)"] + I --> J["낙찰 (승자 1)"] + I --> K["개찰 (낙찰자 미정)"] +``` + +### 2. 1:1 협상 봇 판정 (agent) + +협력사가 가격을 제시할 때마다 봇이 **앵커링가** 기준으로 판정한다. +재제안은 카드를 한 장씩 쓰며 **최대 3번**, 카드 소진·3번 초과에도 앵커 밑으로 못 내리면 실패(투찰 없음). + +```mermaid +flowchart TD + P["협력사 가격 제시"] --> Q{"제시가 vs 앵커링가"} + Q -->|"≤ 앵커링가"| R["협상완료 — 투찰 확정"] + Q -->|"앵커 ~ 앵커×1.02"| S["와일드카드: 1% 인하 요청
(세션당 1회)"] + Q -->|"앵커×1.02 초과"| T["협상 카드로 재제안"] + S --> U{"재제안 횟수 ≤ 3?
카드 남음?"} + T --> U + U -->|"예"| P + U -->|"아니오 (소진·3번 초과)"| V["협상 실패 — 낙찰 후보 아님"] +``` + +### 3. 마감 판정 + +마감 시 **협상완료 세션의 최저 투찰가**를 본다. 공통 전제: 완료 세션이 없거나(전원 미응찰·협상거부) +동가 최저가 2곳 이상이면 유형과 무관하게 **개찰**. 그 외 단독 최저가일 때만 낙찰 후보가 되며, +이후 판정이 유형별로 갈린다. + +#### 3-1. 견적 1:N — 단독 최저면 무조건 낙찰 + +가격 구간을 보지 않는다. 생성 시 `mid/over_action`이 낙찰(AWARD)로 강제되기 때문. + +```mermaid +flowchart TD + M1["마감: 협상완료 세션 최저 투찰가"] --> N1{"완료 세션 있나?"} + N1 -->|"없음 (전원 미응찰·협상거부)"| O1["개찰"] + N1 -->|"동가 최저 2곳+"| O1 + N1 -->|"단독 최저"| X1["낙찰 (가격 구간 무관, 무조건)"] +``` + +#### 3-2. 협상 1:1 — 가격 구간별, 생성 때 정한 값 적용 + +앵커링가 이하는 무조건 낙찰. 그 위 구간은 **견적 생성 때 미리 정해둔 값**(`mid_action`/`over_action`, +1=낙찰·2=개찰)을 마감 시 그대로 적용한다. 두 필드는 적용 구간만 다를 뿐 동작은 동일. + +```mermaid +flowchart TD + M2["마감: 협상완료 세션 최저 투찰가"] --> N2{"완료 세션 있나?"} + N2 -->|"없음 (전원 미응찰·협상거부)"| O2["개찰"] + N2 -->|"동가 최저 2곳+"| O2 + N2 -->|"단독 최저"| W2{"투찰가 위치"} + W2 -->|"≤ 앵커링가"| X2["낙찰"] + W2 -->|"앵커 ~ 목표가"| Y2["생성 시 정한 mid_action 적용
(1=낙찰 / 2=개찰)"] + W2 -->|"목표가 초과"| Z2["생성 시 정한 over_action 적용
(1=낙찰 / 2=개찰)"] +``` + +> 개찰 = 낙찰자 미정 마감(결렬 아님). 개찰 후 수동 처리로 **직접 낙찰 확정**(`/v1/quotation/award`) +> 또는 **재견적 재생성**(`/v1/quotation/regenerate`)이 있다. +> 비즈니스 로직 정본은 [negodata/docs/business-logic.md](negodata/docs/business-logic.md). ## 빠른 시작 ```bash # 1) DB 준비 (최초 1회) — 사용할 PostgreSQL 에 스키마 + 시드 적용 -psql -h 127.0.0.1 -p 5432 -U postgres -f postgres-init/00-init.sql # 스키마 전체 (negosium_db + 도메인·learning·anchoring schema) -psql -h 127.0.0.1 -p 5432 -U postgres -f postgres-init/temp-data.sql # 임시 데이터 시드 (admin / admin1234) +# 스키마 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 -# 2) 백엔드 기동 -docker compose up -d # 두 backend (DB 는 config 대로 외부 연결) -docker compose logs -f +# (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 를 직접 돌려야 새 컬럼이 반영된다). + ## 테스트 ```bash # config.test.toml 의 PostgreSQL(기본 127.0.0.1:5432) 이 떠 있어야 한다 -cd backend # 또는 negodata/backend +cd backend # 또는 negodata/backend, agent, lps ... pip install pytest pytest-asyncio httpx python -m pytest ``` -- httpx `ASGITransport` 로 네트워크 없이 앱을 직접 호출하는 e2e (각 5개). +- httpx `ASGITransport` 로 네트워크 없이 앱을 직접 호출하는 e2e. - `DB_SESSION_MNG` 싱글톤의 커넥션 풀이 첫 이벤트 루프에 묶이므로, 모든 테스트가 단일 session 루프를 공유한다(`pytest.ini`). +- ⚠️ 테스트는 `APP_ENV=test` 로 격리한다(테스트 DB). dev DB(`negosium_db`)에 대고 돌리면 데이터가 날아간다. ## 성능 / 벤치마크 @@ -99,4 +234,9 @@ python -m locust -f loadtest/locustfile.py --host http://localhost:9300 --headle ``` ## 기술 스택 -FastAPI · SQLAlchemy(async) · asyncpg · PostgreSQL 16 · python-jose(JWT) · bcrypt · uvicorn · Docker Compose +- **백엔드**: 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