Initial commit
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
FastAPI 백엔드 API 서버
|
||||
|
||||
- /api/hello: Vue 프론트엔드에서 호출할 JSON API 엔드포인트
|
||||
- / (static): 빌드된 Vue SPA 정적 파일 서빙
|
||||
|
||||
실행:
|
||||
uvicorn app:app --reload --port 8000
|
||||
"""
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from database import engine
|
||||
from models import Base
|
||||
from routers.samples import router as samples_router
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(title="Hello Web API", lifespan=lifespan)
|
||||
|
||||
# CORS 설정 - Vue 개발 서버(localhost:5173)의 요청을 허용
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
app.include_router(samples_router)
|
||||
|
||||
|
||||
# JSON API 엔드포인트
|
||||
@app.get("/api/hello")
|
||||
async def hello_api():
|
||||
"""
|
||||
Vue 프론트엔드가 호출할 API 엔드포인트.
|
||||
|
||||
Returns:
|
||||
dict: 메시지와 초기 카운트 값을 JSON 형태로 반환
|
||||
"""
|
||||
return {
|
||||
"message": "Hello from FastAPI!",
|
||||
"initialCount": 0,
|
||||
}
|
||||
|
||||
|
||||
# Vue 빌드 결과물(정적 파일) 서빙
|
||||
# frontend/dist 폴더를 정적 파일 디렉토리로 마운트
|
||||
app.mount("/", StaticFiles(directory="../frontend/dist", html=True), name="frontend")
|
||||
@@ -0,0 +1,17 @@
|
||||
import os
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
|
||||
DATABASE_URL = os.getenv(
|
||||
"DATABASE_URL",
|
||||
"postgresql+asyncpg://casaos:casaos@192.168.0.60:5432/casaos",
|
||||
)
|
||||
|
||||
engine = create_async_engine(DATABASE_URL, echo=False)
|
||||
|
||||
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
|
||||
async def get_db():
|
||||
async with async_session() as session:
|
||||
yield session
|
||||
@@ -0,0 +1,16 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime, func
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class Sample(Base):
|
||||
__tablename__ = "samples"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
title = Column(String(200), nullable=False)
|
||||
content = Column(Text, nullable=False)
|
||||
author = Column(String(100), nullable=False)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
@@ -0,0 +1,4 @@
|
||||
fastapi==0.115.12
|
||||
uvicorn==0.34.2
|
||||
asyncpg==0.30.0
|
||||
sqlalchemy[asyncio]==2.0.40
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,64 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
|
||||
from database import get_db
|
||||
from models import Sample
|
||||
from schemas import SampleResponse, SampleCreate, SampleUpdate
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["samples"])
|
||||
|
||||
|
||||
@router.get("/samples", response_model=list[SampleResponse])
|
||||
async def list_samples(db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(Sample).order_by(Sample.id))
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.get("/samples/{sample_id}", response_model=SampleResponse)
|
||||
async def get_sample(sample_id: int, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(Sample).where(Sample.id == sample_id))
|
||||
sample = result.scalar_one_or_none()
|
||||
if not sample:
|
||||
raise HTTPException(status_code=404, detail="Sample not found")
|
||||
return sample
|
||||
|
||||
|
||||
@router.post("/samples", response_model=SampleResponse, status_code=201)
|
||||
async def create_sample(body: SampleCreate, db: AsyncSession = Depends(get_db)):
|
||||
sample = Sample(title=body.title, content=body.content, author=body.author)
|
||||
db.add(sample)
|
||||
await db.commit()
|
||||
await db.refresh(sample)
|
||||
return sample
|
||||
|
||||
|
||||
@router.put("/samples/{sample_id}", response_model=SampleResponse)
|
||||
async def update_sample(
|
||||
sample_id: int, body: SampleUpdate, db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
result = await db.execute(select(Sample).where(Sample.id == sample_id))
|
||||
sample = result.scalar_one_or_none()
|
||||
if not sample:
|
||||
raise HTTPException(status_code=404, detail="Sample not found")
|
||||
|
||||
if body.title is not None:
|
||||
sample.title = body.title
|
||||
if body.content is not None:
|
||||
sample.content = body.content
|
||||
if body.author is not None:
|
||||
sample.author = body.author
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(sample)
|
||||
return sample
|
||||
|
||||
|
||||
@router.delete("/samples/{sample_id}", status_code=204)
|
||||
async def delete_sample(sample_id: int, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(Sample).where(Sample.id == sample_id))
|
||||
sample = result.scalar_one_or_none()
|
||||
if not sample:
|
||||
raise HTTPException(status_code=404, detail="Sample not found")
|
||||
await db.delete(sample)
|
||||
await db.commit()
|
||||
@@ -0,0 +1,25 @@
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class SampleResponse(BaseModel):
|
||||
id: int
|
||||
title: str
|
||||
content: str
|
||||
author: str
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class SampleCreate(BaseModel):
|
||||
title: str
|
||||
content: str
|
||||
author: str
|
||||
|
||||
|
||||
class SampleUpdate(BaseModel):
|
||||
title: Optional[str] = None
|
||||
content: Optional[str] = None
|
||||
author: Optional[str] = None
|
||||
@@ -0,0 +1,37 @@
|
||||
import asyncio
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from database import engine, async_session
|
||||
from models import Base, Sample
|
||||
|
||||
|
||||
async def init_db():
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
|
||||
async def seed():
|
||||
await init_db()
|
||||
|
||||
async with async_session() as session:
|
||||
result = await session.execute(select(Sample))
|
||||
existing = result.scalars().all()
|
||||
if existing:
|
||||
print(f"이미 {len(existing)}건의 샘플 데이터가 존재합니다. seed를 건너뜁니다.")
|
||||
return
|
||||
|
||||
samples = [
|
||||
Sample(title="첫 번째 샘플", content="안녕하세요, 첫 번째 게시글입니다.", author="홍길동"),
|
||||
Sample(title="두 번째 샘플", content="PostgreSQL 외부 연결 테스트 중입니다.", author="김철수"),
|
||||
Sample(title="세 번째 샘플", content="FastAPI와 Vue로 게시판을 만듭니다.", author="이영희"),
|
||||
Sample(title="네 번째 샘플", content="데이터베이스 연동이 잘 되네요!", author="박민수"),
|
||||
Sample(title="다섯 번째 샘플", content="화면에 표시되는지 확인해보세요.", author="최지은"),
|
||||
]
|
||||
session.add_all(samples)
|
||||
await session.commit()
|
||||
print(f"{len(samples)}건의 샘플 데이터가 INSERT 되었습니다.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(seed())
|
||||
Reference in New Issue
Block a user