여러 줄 주석이 설명보다 경위(예전·실측·지적)를 적고 있어 읽는 사람이 결론을 찾기 어려웠다. - ts·tsx·js·mjs·css·py 478개: 여러 줄 주석은 첫 문장 한 줄로, 과거형·날짜 문장은 삭제 - 주석 위치는 TypeScript 파서·파이썬 tokenize/ast 로 찾는다 — 문자열 안의 # · /* 는 건드리지 않는다 - eslint·ts·noqa·type: ignore 같은 지시 주석은 그대로 둔다 파이썬 275개 정리 전후 AST 동일, TS 298개 주석 뺀 토큰 동일(빈 JSX 주석 10곳만 차이). site·frontend·admin tsc, site vitest 105 passed Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
136 lines
4.9 KiB
Python
136 lines
4.9 KiB
Python
import time
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI, Request, Response
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.middleware.gzip import GZipMiddleware
|
|
from sqlalchemy import text
|
|
|
|
from common.database.db_session_manager import DB_SESSION_MNG
|
|
from common.enums import DBType, DBWRType
|
|
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.place.place
|
|
import router.v1.fact.fact
|
|
import router.v1.faq.faq
|
|
import router.v1.media.media
|
|
import router.v1.media.relay
|
|
import router.v1.job.job
|
|
import router.v1.site.site
|
|
import router.v1.site.showcase
|
|
import router.v1.site.booking_request
|
|
import router.v1.site.post
|
|
import router.v1.site.review
|
|
import router.v1.local.local
|
|
import router.v1.ops.ops
|
|
import router.v1.social.social
|
|
import router.v1.social.oauth
|
|
import router.v1.agent.kakao
|
|
import router.v1.agent.chat
|
|
import router.v1.agent.kakao_bot
|
|
|
|
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="Web4Ai API", lifespan=lifespan)
|
|
|
|
# CORS — 관리자 프론트(client_url) + 랜딩(landing_url, 미설정이면 제외).
|
|
def _origins(*values: str) -> list[str]:
|
|
seen: list[str] = []
|
|
for value in values:
|
|
for origin in value.split(","):
|
|
origin = origin.strip().rstrip("/")
|
|
if origin and origin not in seen:
|
|
seen.append(origin)
|
|
return seen
|
|
|
|
|
|
ALLOWED_ORIGINS = _origins(web_server_config.client_url, web_server_config.landing_url)
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=ALLOWED_ORIGINS,
|
|
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)
|
|
if request.url.path.startswith('/v1/social/'):
|
|
response.headers['Cache-Control'] = 'no-store'
|
|
response.headers['Referrer-Policy'] = 'no-referrer'
|
|
response.headers['X-Robots-Tag'] = 'noindex, nofollow'
|
|
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
|
|
|
|
|
|
@app.get(path="/readyz", responses={404: {"description": "Not found"}, 503: {"description": "Not ready"}})
|
|
async def readyz(response: Response):
|
|
"""healthz 와 다른 걸 본다 — healthz 는 "프로세스가 살아 있나"(항상 200), 이건 "요청을 실제로 처리할 수 있나"(DB 에 붙는지 실제로 한 번 물어본다)."""
|
|
try:
|
|
async def _ping(s):
|
|
await s.execute(text("SELECT 1"))
|
|
return True
|
|
|
|
await DB_SESSION_MNG.execute_lambda(DBType.MAIN.value, DBWRType.DB_READ.value, _ping)
|
|
return {"ok": True, "db": "up"}
|
|
except Exception as ex: # noqa: BLE001 — 준비 안 됐다는 것 자체가 이 엔드포인트의 응답이다
|
|
LOG.w(f"[readyz] DB 연결 확인 실패: {type(ex).__name__}: {ex}")
|
|
response.status_code = 503
|
|
return {"ok": False, "db": "down"}
|
|
|
|
|
|
# 각 도메인 라우터를 등록한다.
|
|
app.include_router(router.v1.auth.account.router)
|
|
app.include_router(router.v1.place.place.router)
|
|
app.include_router(router.v1.fact.fact.router)
|
|
app.include_router(router.v1.faq.faq.router)
|
|
app.include_router(router.v1.media.media.router)
|
|
# 인증 없는 공개 중계.
|
|
app.include_router(router.v1.media.relay.router)
|
|
app.include_router(router.v1.job.job.router)
|
|
app.include_router(router.v1.site.site.router)
|
|
app.include_router(router.v1.site.site.my_router)
|
|
# 인증 없는 공개 목록.
|
|
app.include_router(router.v1.site.showcase.router)
|
|
app.include_router(router.v1.site.booking_request.router)
|
|
app.include_router(router.v1.site.post.router)
|
|
app.include_router(router.v1.site.post.owner_router)
|
|
app.include_router(router.v1.site.review.router)
|
|
app.include_router(router.v1.local.local.router)
|
|
app.include_router(router.v1.local.local.weather_router)
|
|
app.include_router(router.v1.ops.ops.router)
|
|
|
|
app.include_router(router.v1.social.social.router)
|
|
app.include_router(router.v1.social.oauth.router)
|
|
app.include_router(router.v1.agent.kakao.router)
|
|
app.include_router(router.v1.agent.chat.router)
|
|
app.include_router(router.v1.agent.kakao_bot.router)
|