o2o-site-AEO/admin/backend/app.py
Mina Choi 872d00f3c4 [feat] solution: 미니 블로그·이용후기·예약요청 추가, /s/stay 목업·발행 사이트 UI 다수 수정
인앱 미니 블로그(AI 자동 포스트, 이메일 승인)·이용후기(즉시 게시)·예약 요청(메일 발송)을
새로 붙였고, 병행해서 /s/stay 목업과 발행 사이트 공통 렌더러(UnitsSection·FestivalSection·
LocalGuideSection·WeatherSection 등)의 UI 버그를 다수 고쳤다. 범위가 넓지만 한 주 분량
작업을 한 커밋으로 묶어 달라는 요청에 따라 하나로 묶는다.

- solution/backend: post/review/booking_request 라우터·서비스·CRUD 추가, 스케줄러에
  블로그 초안 생성(새벽 4:10)·발송(아침 9:00) cron 등록, 마이그레이션 4건 추가
- solution/frontend, admin/frontend: 생성된 API 클라이언트 갱신, 리뷰 모더레이션·
  블로그 글 관리 페이지 추가
- solution/site/src: 객실 상세+실시간예약(날짜선택·연락처 폼)을 모달로 통합, 축제·
  주변안내 카드 클릭 시 모달 전환, 후기 목록 카드 UI, 공용 Modal 컴포넌트 신설,
  날씨 문구 동기화 버그 수정(하늘줄·기온줄 한 타이머로), 시설·편의 가능/불가 아이콘
  색상 하이라이트, 헤더 메뉴 순서를 실제 섹션 순서에 맞춤, 하단 탭바 아이콘 정렬 버그
  (line-height) 수정, 추천일정 점선 연결+데스크톱 자동펼침/모바일 축소, 채널 라벨에
  크롤링 원문("NOL")이 새던 것을 bookingLabel() 로 교체
- solution/site/scripts/mockup: /s/stay 패치 스크립트·주입 CSS·JS 다수 수정, stay4~6
  빌드 스크립트 추가(다른 세션 작업)

테스트: solution/site `npx tsc --noEmit` 통과, `npx vitest run` 93 passed,
solution/backend `pytest tests/test_booking_request.py` 6 passed(로컬 DB 대상).
예약 요청 메일은 실제 발송까지 확인(place 66894a1b 소유자 이메일 누락을 DB에서 보정).
2026-09-18 09:03:18 +09:00

89 lines
2.9 KiB
Python

"""어드민 API. 솔루션 API(:9800)와 프로세스·포트가 갈린다.
도메인 코드는 solution/backend 것을 PYTHONPATH 로 쓴다 — admin 전용 라우터가 0개라
(전부 place·fact) 새로 쓰면 같은 테이블을 두 벌 구현하는 것뿐이다.
경로 접두어가 아니라 포트를 가른 이유: 접두어는 같은 프로세스라 사장님이 닿는 서버에
내부 엔드포인트가 존재한다. 포트를 가르면 아예 없다.
"""
import time
from fastapi import Depends, 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 router.v1.validator.dependencies import RequireDeveloper
import router.v1.auth.account
import router.v1.fact.fact
import router.v1.job.job
import router.v1.local.local
import router.v1.place.place
import router.v1.site.review_admin
import router.v1.site.site
from contextlib import asynccontextmanager
API_SERVER_START_TIME = GTime.UTCStr()
@asynccontextmanager
async def lifespan(app: FastAPI):
# 크론은 :9800 담당. 여기서도 돌리면 같은 시각에 중복 실행된다.
yield
await DB_SESSION_MNG.dispose_all()
app = FastAPI(title="Web4Ai Admin API", lifespan=lifespan)
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)
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
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)
LOG.d(f"[admin] {response.status_code} {request.method} {request.url.path} - {time.time() - start_time:.4f}s")
return response
@app.get(path="/healthz")
async def healthz():
return API_SERVER_START_TIME
# auth 만 게이트 밖 — 로그인까지 막으면 아무도 못 들어온다.
app.include_router(router.v1.auth.account.router)
# 나머지 전부 role >= DEVELOPER.
_gate = [Depends(RequireDeveloper)]
app.include_router(router.v1.place.place.router, dependencies=_gate)
app.include_router(router.v1.fact.fact.router, dependencies=_gate)
app.include_router(router.v1.job.job.router, dependencies=_gate)
app.include_router(router.v1.site.site.router, dependencies=_gate)
app.include_router(router.v1.local.local.router, dependencies=_gate)
app.include_router(router.v1.site.review_admin.router, dependencies=_gate)