Initial commit

This commit is contained in:
2026-05-29 08:11:07 +09:00
commit 9d192c430d
824 changed files with 575587 additions and 0 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+61
View File
@@ -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")
+17
View File
@@ -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
+16
View File
@@ -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())
+4
View File
@@ -0,0 +1,4 @@
fastapi==0.115.12
uvicorn==0.34.2
asyncpg==0.30.0
sqlalchemy[asyncio]==2.0.40
View File
Binary file not shown.
Binary file not shown.
+64
View File
@@ -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()
+25
View File
@@ -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
+37
View File
@@ -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())