agent(협상카드 강화학습)와 anchoring(구간별 자동 조정)이 쌓아온 결과를 볼 수 있는 화면이 없어 DB 를 직접 열어야 확인이 됐다. 두 축은 성격이 달라 한 화면에 섞지 않고 탭으로 나눈다. 백엔드(읽기 전용 — 값의 주인은 agent·anchoring): - crud/learning_crud.py: learning·anchoring 스키마 경량 조회(negodata ORM 미매핑 테이블) - services/learning_service.py + router/v1/learning: /v1/learning/cards · /anchoring 프론트: - 사이드바 '협상 학습'(/learning) 추가, 탭 = 협상카드 | 앵커링 - 협상카드: 학습 협상·기록·타결 비율 요약 + 카드별 사용/타결/평균 점수(0 기준 좌우 막대) - 앵커링: 구간별 현재 인하폭 + 조정 이력(표본·성공률·값 변화) 검증: 로컬 실협상 3건으로 데이터 생성 후 두 탭 실화면 확인, tsc·eslint 통과.
86 lines
3.0 KiB
Python
86 lines
3.0 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.company.settings
|
|
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.learning.learning
|
|
import router.v1.statistics.statistics
|
|
import router.v1.notification.notification
|
|
import router.v1.renegotiation.renegotiation
|
|
|
|
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.company.settings.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.learning.learning.router)
|
|
app.include_router(router.v1.notification.notification.router)
|
|
app.include_router(router.v1.renegotiation.renegotiation.router)
|