협상 근거로 쓰는 화면이라 "얼마에 살 수 있고, 그게 어디서 나온 값인가"를
한 번에 검증할 수 있게 정보 구조를 다시 짰다.
구조: 결론 → 판단 → 근거
- 히어로: 합계(상품가+배송비) 32px + 단가 대비 판정을 한 블록에.
면은 브랜드보다 채도를 낮춘 --accent 표면, 값만 브랜드 컬러(화면에 단 하나).
- 판매처 비교표: 상품가/배송비/합계를 몰별로. 상품가 최저와 실구매가 최저가
다를 수 있어 둘 다 표시. 결과 없는 몰도 '미발견' 행으로 남겨 빈손임을 드러낸다.
- 검색어(찾은 상품명) + 복사 버튼 → 판매처에서 다시 찾아볼 때 붙여넣기용.
- 가격 추이: 상품가 단일 기준. 합계 기준은 배송비를 아는 회차에만 값이 생겨
(네이버는 배송비 미제공 — 실측 39/39) 기준을 바꾸면 값의 의미와 표본 수가
동시에 변한다. 기준선(상품 단가) 추가 — 이 선 위면 사는 게 손해.
- 수집 이력: 미발견도 노출(전체의 30%가 not_found 인데 그동안 숨겨져 있었다).
데이터 정직성
- 배송비 미상(전체 69%)을 0원으로 뭉개지 않는다 — 무료배송으로 읽히면 안 된다.
모르면 히어로 라벨이 '합계' → '상품가'로 바뀌고 그 사실을 문구로 밝힌다.
- 화면 주인공(합계)과 목표가 산정에 쓰이는 값(상품가)이 다르므로 각주로 명시.
- 금액 옆 배송 주석은 '+3,500'(연산으로 오해) → '배송비 포함(3,500원)'.
접근성·일관성
- 대비 전수 계산: --accent 면 위에서 muted-fg(4.16)·amber-700(4.28)이 AA 미달이라
accent-foreground(9.85)·800 계열(6.0~6.5)로 교체. 히어로는 큰 글씨 기준 통과.
- rose(파괴적 뉘앙스) 제거 → 브랜드 인디고 한 톤. 장식 아이콘 전부 제거.
- 타이포 7단계를 상수 T 로 고정(임의 크기 금지).
- 차트 점 3종 모양을 DOT 상수로 추출해 차트와 범례가 공유 — 범례는 차트 밖이라
--color-price 가 해석되지 않아 색이 어긋났다. 전역 토큰으로 통일.
- Y축 눈금 1·2·5×10ⁿ, 기준 변경 시 애니메이션 끔, 커스텀 X축 tick 제거
(recharts 의 가장자리 앵커 보정을 건너뛰어 점과 라벨이 어긋났다).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|---|---|---|
| .. | ||
| public/fonts | ||
| src | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| components.json | ||
| Dockerfile | ||
| eslint.config.js | ||
| index.html | ||
| orval.config.ts | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| tsconfig.tsbuildinfo | ||
| vite.config.ts | ||
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)).
- 단, 사람이 읽을 수 있는 메시지로 가공한 뒤 다시 throw 하는 건 허용
(예:
- UI 계층(페이지/컴포넌트) —
try/catch로 잡아서 화면에 반영한다 (예:pages/login.tsx의handleSubmit→setError(...)).
예외: 에러를 무시해도 되는 동작은 그 자리에서 삼킨다.
예) service.ts 의 logout — 서버 호출이 실패해도 로컬 토큰만 정리하면 되므로 UI까지 올리지 않는다.
| 계층 | 에러 태도 |
|---|---|
| orval / API 호출 | 실패 발생 (raw) |
callAuth 같은 래퍼 |
잡아서 다듬고 다시 throw |
| service / hook | 그냥 통과 (안 잡음) |
| UI (페이지) | catch → 화면 표시 |