완료 판정 버그(로딩이 안 멈추던 원인 중 하나)
폴링이 crawl_end_time 을 문자열로 비교했다. 서버는 마이크로초 6자리(.755705Z),
JS toISOString() 은 밀리초 3자리(.755Z)라 자릿수가 달라 사전순이 시간순과 어긋난다
('.' < 'Z'). 게다가 클라이언트 시각을 서버 타임스탬프의 기준점으로 써서
브라우저 시계가 조금만 앞서도 어떤 결과도 기준을 넘지 못했다.
→ 요청 직전 서버가 준 최신 수집 시각을 상품별 기준선으로 읽고 epoch ms 로 비교.
UI 재설계 (소비자 앱풍 → B2B 실무 툴)
- 상태 3분리: confirm | searching | done. 검색 중에 완료 결론을 섞지 않는다
- 표가 주인공: [선택 | 상품 | 기존 최저가 | 네이버 | 쿠팡 | 결과], 컴팩트 행, tabular-nums
- 결과 열은 상태어(탐색 성공 / 변동 없음 / 탐색 실패 / 탐색 중)
- 진행 카드: 표시등 + mm:ss 경과 + 진행바 + N/M, 셀 단위 스켈레톤으로 진행 위치 표시
- radius 8/6/4, shadow 최소화, CTA 우측 정렬(풀와이드 금지), 강조는 indigo-800 한 톤
- 완료 후 자동으로 닫지 않는다(결과 확인). 선택 해제는 닫을 때로 미룸
선택 재검색
체크박스로 대상을 고르고, 검색이 끝나면 갱신되지 않은 행만 자동 선택된다
(이미 갱신된 상품에 크롤 비용을 다시 쓰지 않도록). 재검색은 force=true 로 나가
네거티브 캐시를 우회한다.
접근성 (스킬 기준 + 대비 실측)
- 보조 텍스트 muted-foreground 4.88:1, indigo-800 9.93:1 (기존 1.92:1 FAIL 해소)
- 상태는 색 단독이 아니라 텍스트/굵기/sr-only 병행
- role=status aria-live, 탈출구 3중(X·ESC·오버레이), 버튼 높이 통일
api/generated 는 orval 재생성분. resTargetBreakdown 변경은 이전 백엔드 수정과의
스펙 동기화(이번 작업과 무관하지만 재생성으로 함께 정리됨).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|---|---|---|
| agent | ||
| backend | ||
| frontend | ||
| landing | ||
| lps | ||
| lps-admin | ||
| negodata | ||
| postgres-init | ||
| schedules/anchoring | ||
| scripts | ||
| .gitignore | ||
| docker-compose.prod.landing.yml | ||
| docker-compose.yml | ||
| IMK_상품_업로드.csv | ||
| IMK_협력사_업로드.csv | ||
| logs.sh | ||
| o2o_상품_업로드.csv | ||
| o2o_협력사_업로드.csv | ||
| README.md | ||
O2O Negosium
동일 구조의 두 서비스(negosium, negodata)가 하나의 PostgreSQL 인스턴스를 공유한다.
구성
o2o-negosium/
├── docker-compose.yml # 두 backend (DB 는 외부)
├── postgres-init/ # DB·테이블 셋업 SQL (대상 DB 에 1회 적용)
├── backend/ # negosium 백엔드 (포트 9300)
├── negodata/backend/ # negodata 백엔드 (포트 9400)
├── agent/ front/ # (예정)
└── negodata/front/ # (예정)
두 백엔드는 같은 코드 골격(MVC · 람다 DB · Depends 주입 · JWT 로그인)을 쓴다. 아키텍처/패턴 상세는 각 서버 README 참고: backend · negodata/backend
DB 는 compose 밖 (config 로 연결)
DB 는 docker-compose 에서 관리하지 않는다. 각 backend 는 config.<APP_ENV>.toml 의 접속 정보대로
외부 PostgreSQL(호스트 로컬 postgres, 또는 따로 떠 있는 docker postgres)에 연결한다.
한 PostgreSQL 안에 서비스별 database 를 둔다.
PostgreSQL (외부, 5432)
├── negosium_db ← negosium-backend
└── negodata_db ← negodata-backend
- 컨테이너(docker env)에서 호스트 DB 접근:
host.docker.internal:5432(config.docker.toml) - 로컬 실행/테스트(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) 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)
# 2) 백엔드 기동
docker compose up -d # 두 backend (DB 는 config 대로 외부 연결)
docker compose logs -f
docker compose down
테스트
# config.test.toml 의 PostgreSQL(기본 127.0.0.1:5432) 이 떠 있어야 한다
cd backend # 또는 negodata/backend
pip install pytest pytest-asyncio httpx
python -m pytest
- httpx
ASGITransport로 네트워크 없이 앱을 직접 호출하는 e2e (각 5개). DB_SESSION_MNG싱글톤의 커넥션 풀이 첫 이벤트 루프에 묶이므로, 모든 테스트가 단일 session 루프를 공유한다(pytest.ini).
성능 / 벤치마크
/login 은 bcrypt(CPU 바운드) 가 비용의 대부분이다. 초기에는 bcrypt 가 asyncio
이벤트 루프를 막아 아무 일도 안 하는 /healthz 조차 p99 3.3s 가 나왔다.
두 가지 최적화
- bcrypt 를
asyncio.to_thread로 오프로드 — 이벤트 루프 비차단. bcrypt 는 해싱 중 GIL 을 해제하므로 스레드들이 여러 코어에서 실제 병렬 실행된다. - 워커 수 증가 (
process_count1 → 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 은 초기 계정생성 버스트 후 바닥으로 떨어진다.
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 · Docker Compose
