o2o-negosium-original/negodata/front
Mina Choi 6eb4dc26f8 [feat] negosium·negodata: 협상 거부 흐름 정리 + 오프라인 계약가 낙찰 + 견적 상세 리모델링
협상 불가 사유를 내면 500 이 나고, 거부 폼은 목록·채팅이 따로 놀았으며,
제출한 내용을 다시 볼 방법이 없었다. 결렬 건은 협력사가 낸 거부가를 계약가로
간주해 낙찰시켜 절감 통계가 음수로 뒤집힐 수 있었고, 견적 상세는 판정 가격이
세 곳에 흩어져 대화 탭에선 아예 보이지 않았다.

agent
- 결렬 종료 로깅 크래시 수정 — _log 를 action_id 기반으로 되돌리고 선택 근거
  (Q·UCB·방문수)는 decision/policy 가 있을 때만 채운다. 종료 행은 카드 선택이
  없고 policy.update 뒤라 값을 넣으면 학습 화면 집계가 오염된다

negosium
- 거부 폼을 목록·채팅 공용 컴포넌트 하나로 통일(사유 3종 + 공급 희망가·의견 선택)
- 거부 사유 열람 — 목록에 '거부 사유 보기'(부가정보 보기와 같은 규격), 채팅
  재진입 시 대화 끝에 거부 내역 카드. 목록·채팅 init 응답에 reject_reason·reject_price 추가
- 자유 입력 거부("협상 포기합니다")가 사유 NULL 로 저장되던 문제 수정 — 폼 마커가
  없으면 원문을 사유로 쓰고, 문장 속 숫자를 희망가로 오인하지 않는다
- koreanNumber 를 전역 lib 으로 이동(공용 폼이 쓴다)

negodata
- 직접 낙찰에 계약가 입력 — 결렬·미응찰 건을 오프라인으로 다시 협상한 결과를
  담당자가 확정해 넣는다. 후보는 초청 협력사 전부(가격 미제출도 포함),
  계약가는 sessions.custom.offline_award 에 근거·작성자·시각과 함께 남긴다
- 통계 계약가 = 담당자 확정가 우선, 없으면 투찰가. 거부가를 계약가로 치던 파생 제거.
  KPI 에 오프라인 반영 건수 추가
- 견적 상세 리모델링 — 가격 레일(앵커링가/투찰현황 · 목표가 · 타결 상한가 · 결과가)을
  시트에 고정해 접힘·탭 전환에도 남기고, 스펙트럼에 타결 판정선과 구간색 추가.
  라벨은 폭을 실측해 두 레인으로 배치(겹침 불가). 상품·마감시각 등 전 행 동일 컬럼 제거,
  협상현황에 부가정보 노출, 1:1 은 협력사·세션상태를 결과 밴드로 올림

테스트: negosium 58 · negodata 110 통과. 프론트 빌드/린트 통과.
2026-08-12 10:25:26 +09:00
..
public/fonts [feat] negodata/front: 프론트 초기 구축 (Vite+React, orval API 클라이언트, 견적/상품/협력사 페이지) 2026-06-16 17:18:08 +09:00
src [feat] negosium·negodata: 협상 거부 흐름 정리 + 오프라인 계약가 낙찰 + 견적 상세 리모델링 2026-08-12 10:25:26 +09:00
.dockerignore [feat] negodata/front: 프론트 초기 구축 (Vite+React, orval API 클라이언트, 견적/상품/협력사 페이지) 2026-06-16 17:18:08 +09:00
.env.example [feat] negodata/front: 프론트 초기 구축 (Vite+React, orval API 클라이언트, 견적/상품/협력사 페이지) 2026-06-16 17:18:08 +09:00
.gitignore [feat] negodata/front: 프론트 초기 구축 (Vite+React, orval API 클라이언트, 견적/상품/협력사 페이지) 2026-06-16 17:18:08 +09:00
components.json [feat] negodata/front: 프론트 초기 구축 (Vite+React, orval API 클라이언트, 견적/상품/협력사 페이지) 2026-06-16 17:18:08 +09:00
Dockerfile cors 문제 해결 . 2026-06-18 16:30:03 +09:00
eslint.config.js [chore] 작업분 일괄: logs.sh 컨테이너 메뉴화 · dbeaver 초기화 SQL · landing 운영 compose · negodata/front eslint 도입 · 업로드 샘플 CSV 2026-07-24 18:45:46 +09:00
index.html [fix] negodata/front: 윈도우 모달 한글 깨짐 — 컨테이너 font-mono 제거 + 모노 스택 한글 폴백 2026-08-10 09:49:26 +09:00
orval.config.ts [feat] negodata/front: 프론트 초기 구축 (Vite+React, orval API 클라이언트, 견적/상품/협력사 페이지) 2026-06-16 17:18:08 +09:00
package-lock.json [refactor] negodata/front: 견적등록 모달 훅 분리 + 시장가 모순 카드 차단(10번), 엑셀 업로드 xlsx 파싱·CP949 폴백(7번) 2026-08-03 09:33:06 +09:00
package.json [refactor] negodata/front: 견적등록 모달 훅 분리 + 시장가 모순 카드 차단(10번), 엑셀 업로드 xlsx 파싱·CP949 폴백(7번) 2026-08-03 09:33:06 +09:00
README.md [docs] negodata: README 갱신 — 테스트 실행법·API 도메인·프론트 포트 2026-07-01 15:06:55 +09:00
tsconfig.json [feat] negodata/front: 프론트 초기 구축 (Vite+React, orval API 클라이언트, 견적/상품/협력사 페이지) 2026-06-16 17:18:08 +09:00
tsconfig.tsbuildinfo 와일들카드 선택 수정 . 2026-07-07 17:16:19 +09:00
vite.config.ts [feat] negodata/front: 프론트 초기 구축 (Vite+React, orval API 클라이언트, 견적/상품/협력사 페이지) 2026-06-16 17:18:08 +09:00

negodata 프론트엔드

negosium/negodata 협상 플랫폼의 웹 프론트엔드.

스택

  • 빌드 / 런타임: Vite 6 + React 19 + TypeScript
  • 라우팅: react-router v7 (createBrowserRouter)
  • 서버 상태: TanStack Query (React Query)
  • UI: Tailwind CSS v4 + shadcn/ui, lucide-react, sonner
  • 폼 / 검증: react-hook-form + zod
  • 전역 상태: zustand
  • API 클라이언트: orval (백엔드 OpenAPI → 타입·React Query 훅 자동생성)

실행

이 프론트는 단독으로 띄우지 않는다. 최상위 negosium 폴더(저장소 루트)의 docker compose 로 DB·백엔드와 함께 기동한다.

# 저장소 루트(o2o-negosium)에서 — 전체 스택(DB + 백엔드 2개 + 프론트)
docker compose up -d --build
  • 프론트: http://localhost:3000 (compose 서비스 negodata-front, 3000:3000 매핑)
  • 소스를 바인드마운트하므로 코드 수정은 HMR 로 자동 반영된다.
  • (참고: 공급사용 negosium-front 는 별개로 :3300)

프론트만 단독 개발 (선택)

도커 없이 이 폴더만 띄울 때:

npm install
cp .env.example .env   # 환경변수 (아래 표)
npm run dev            # http://localhost:3000

환경변수 (.env)

.env.example 을 복사해서 만든다. 실제 .env 는 git 에 안 올라간다(.gitignore).

변수 기본값 설명
VITE_API_BASE_URL http://localhost:9400 백엔드 API 주소. 빌드 시 정적으로 구워진다(운영 빌드는 build arg 로도 전달).

ORVAL_INPUT 은 .env 가 아니라 orval 실행 시 쓰는 셸 환경변수다 (아래 참고).

스크립트

명령 설명
npm run dev 개발 서버 (vite, :3000)
npm run build 프로덕션 빌드
npm run preview 빌드 결과 미리보기
npm run lint 타입 체크 (tsc --noEmit)
npm run orval 백엔드 OpenAPI 로 API 클라이언트 재생성 (아래)

API 클라이언트 생성 (orval)

백엔드 FastAPI 의 OpenAPI 스펙을 읽어 src/api/generated 에 React Query 훅 + 타입을 생성한다. 설정은 orval.config.ts.

# 기본: 백엔드를 :9400 으로 띄운 뒤 라이브 스펙에서 생성
npm run orval

# 백엔드 없이: 저장된 openapi.json 파일을 가리켜 생성
ORVAL_INPUT=../backend/openapi.json npm run orval
  • 입력: http://localhost:9400/openapi.json (또는 ORVAL_INPUT 으로 지정한 파일)
  • 출력: src/api/generated/** (tags-split, 모델은 generated/model)
  • 산출물은 자동생성물 — 직접 수정 금지. 백엔드 API 가 바뀌면 다시 실행한다.
  • 모든 생성 호출은 공통 mutator src/api/mutator/custom-fetch.ts 를 통과한다 (baseURL·토큰·에러 처리). baseURL 은 VITE_API_BASE_URL 을 쓴다.
  • FastAPI operationId(list_items_v1_item_list_get)는 _v1_ 앞부분만 취해 camelCase 로 정리 → listItems / useListItems 형태가 된다.

디렉토리 구조

src/
  app/           # 엔트리(main), router, provider
  api/
    generated/   # orval 자동생성 (직접 수정 금지)
    mutator/     # custom-fetch (요청 공통 로직: baseURL·토큰·에러)
  features/      # 도메인별: auth, quotations(견적), products(상품), partners(협력사), cards(협상카드), dashboard, members(회원관리), onboarding
  components/    # ui(shadcn), layout
  pages/         # 화면
  stores/        # zustand 스토어
  lib/           # 공용 유틸

코딩 컨벤션

에러 처리 규칙

에러는 던지는 곳(service/hook) 과 잡는 곳(UI) 을 분리한다.

  • service / hook 계층 — 에러를 처리하지 않는다. 실패하면 throw 로 위로 전파만 한다. 화면을 모르므로 "에러를 어떻게 보여줄지" 를 결정하지 않는다.
    • 단, 사람이 읽을 수 있는 메시지로 가공한 뒤 다시 throw 하는 건 허용 (예: features/auth/service.ts 의 callAuth — ApiError → Error(error_message)).
  • UI 계층(페이지/컴포넌트) — try/catch 로 잡아서 화면에 반영한다 (예: pages/login.tsx 의 handleSubmit → setError(...)).

예외: 에러를 무시해도 되는 동작은 그 자리에서 삼킨다. 예) service.ts 의 logout — 서버 호출이 실패해도 로컬 토큰만 정리하면 되므로 UI까지 올리지 않는다.

계층 에러 태도
orval / API 호출 실패 발생 (raw)
callAuth 같은 래퍼 잡아서 다듬고 다시 throw
service / hook 그냥 통과 (안 잡음)
UI (페이지) catch → 화면 표시