o2o-negosium-original/negodata/backend/router/router.py
Mina Choi c32ee300e6 [feat] negodata: 통계 페이지(성과 분석) 추가 — 파생 집계 API + recharts 차트
- 백엔드 GET /v1/statistics/summary: 회사/내견적 스코프, 최근 6개월 파생 집계(저장 X, DDL 0)
- 지표: 총절감/절감률/낙찰률/앵커도달률/마감수/재견적라운드 + 월별추이·마감결과분해·참여율·유형별(협상/견적)·카테고리·카드빈도
- 프론트 recharts+shadcn Chart, 사이드바 통계 nav, /statistics

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 16:18:33 +09:00

80 lines
2.8 KiB
Python

import time
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from common.database.db_session_manager import DB_SESSION_MNG
from common.logger import LOG
from common.utils.gtime import GTime
from config.server_configs import web_server_config
from scheduler import shutdown_scheduler, start_scheduler
import router.v1.auth.account
import router.v1.company.user
import router.v1.item.item
import router.v1.supplier.supplier
import router.v1.supplier_item.supplier_item
import router.v1.card.card
import router.v1.quotation.quotation
import router.v1.quotation_setting.quotation_setting
import router.v1.dashboard.dashboard
import router.v1.statistics.statistics
import router.v1.notification.notification
API_SERVER_START_TIME = GTime.UTCStr()
@asynccontextmanager
async def lifespan(app: FastAPI):
# startup: 마감 스케줄러 기동(SCHEDULER_ENABLED=1 인 프로세스에서만)
start_scheduler()
yield
# shutdown: 스케줄러 정지 + DB 엔진 커넥션 풀 정리
shutdown_scheduler()
await DB_SESSION_MNG.dispose_all()
app = FastAPI(title="Negodata Api Server", lifespan=lifespan)
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=[web_server_config.client_url],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Accept-Encoding: gzip 요청에 대해 1000 bytes 이상 응답을 압축.
app.add_middleware(GZipMiddleware, minimum_size=1000)
@app.middleware("http")
async def log_time(request: Request, call_next):
start_time = time.time()
response = await call_next(request)
elapsed = time.time() - start_time
# status_code 를 함께 남긴다(403/4xx 등을 로그만으로 식별 가능하게).
LOG.d(f"{response.status_code} {request.method} {request.url.path} - {elapsed:.4f}s")
return response
@app.get(path="/healthz", responses={404: {"description": "Not found"}})
async def healthz():
return API_SERVER_START_TIME
# 각 도메인 라우터를 등록한다. 새 기능 추가 시 router.v1.<domain>.<file> 를 import 후 include.
app.include_router(router.v1.auth.account.router)
app.include_router(router.v1.company.user.router)
app.include_router(router.v1.item.item.router)
app.include_router(router.v1.supplier.supplier.router)
app.include_router(router.v1.supplier_item.supplier_item.router)
app.include_router(router.v1.card.card.router)
app.include_router(router.v1.quotation.quotation.router)
app.include_router(router.v1.quotation_setting.quotation_setting.router)
app.include_router(router.v1.dashboard.dashboard.router)
app.include_router(router.v1.statistics.statistics.router)
app.include_router(router.v1.notification.notification.router)