from fastapi import FastAPI, HTTPException, Path, status from fastapi.middleware.cors import CORSMiddleware import uvicorn import yaml import os import uuid import aiosqlite from fastapi.responses import HTMLResponse, FileResponse app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None) from database import init_db, DB_PATH from models import Post, PostCreateRequest, PostUpdateRequest, Comment, CommentCreateRequest, CommentUpdateRequest, LikeRequest # 댓글 목록 조회 @app.get("/api/posts/{postId}/comments", response_model=list[Comment]) async def list_comments(postId: str = Path(...)): async with aiosqlite.connect(DB_PATH) as db: db.row_factory = aiosqlite.Row async with db.execute("SELECT id FROM posts WHERE id = ?", (postId,)) as cursor: post = await cursor.fetchone() if not post: raise HTTPException(status_code=404, detail="게시물을 찾을 수 없음") async with db.execute("SELECT * FROM comments WHERE postId = ? ORDER BY createdAt ASC", (postId,)) as cursor: rows = await cursor.fetchall() return [Comment(**dict(row)) for row in rows] # 댓글 생성 @app.post("/api/posts/{postId}/comments", response_model=Comment, status_code=status.HTTP_201_CREATED) async def create_comment(req: CommentCreateRequest, postId: str = Path(...)): comment_id = str(uuid.uuid4()) now = now_iso() comment = Comment( id=comment_id, postId=postId, username=req.username, content=req.content, createdAt=now, updatedAt=now ) import os print(f"[DEBUG][create_comment] 진입: postId={postId}, DB_PATH={DB_PATH}, exists={os.path.exists(DB_PATH)}", flush=True) async with aiosqlite.connect(DB_PATH) as db: await db.execute("PRAGMA journal_mode=WAL;") db.row_factory = aiosqlite.Row print(f"[DEBUG][create_comment] 쿼리: SELECT id FROM posts WHERE id = '{postId}'", flush=True) async with db.execute("SELECT id FROM posts WHERE id = ?", (postId,)) as cursor: post = await cursor.fetchone() print(f"[DEBUG][create_comment] 쿼리 결과: postId={postId}, row={post}", flush=True) if not post: print(f"[ERROR][create_comment] postId={postId}로 posts 테이블에서 조회 실패", flush=True) raise HTTPException(status_code=404, detail="게시물을 찾을 수 없음") await db.execute( """ INSERT INTO comments (id, postId, username, content, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?) """, (comment.id, comment.postId, comment.username, comment.content, comment.createdAt, comment.updatedAt) ) await db.execute("UPDATE posts SET comments = comments + 1 WHERE id = ?", (postId,)) await db.commit() return comment # 특정 댓글 조회 @app.get("/api/posts/{postId}/comments/{commentId}", response_model=Comment) async def get_comment(postId: str, commentId: str): async with aiosqlite.connect(DB_PATH) as db: db.row_factory = aiosqlite.Row async with db.execute("SELECT * FROM comments WHERE id = ? AND postId = ?", (commentId, postId)) as cursor: row = await cursor.fetchone() if not row: raise HTTPException(status_code=404, detail="댓글을 찾을 수 없음") return Comment(**dict(row)) # 댓글 업데이트 @app.patch("/api/posts/{postId}/comments/{commentId}", response_model=Comment) async def update_comment(postId: str, commentId: str, req: CommentUpdateRequest): now = now_iso() async with aiosqlite.connect(DB_PATH) as db: db.row_factory = aiosqlite.Row async with db.execute("SELECT * FROM comments WHERE id = ? AND postId = ?", (commentId, postId)) as cursor: row = await cursor.fetchone() if not row: raise HTTPException(status_code=404, detail="댓글을 찾을 수 없음") await db.execute( """ UPDATE comments SET username = ?, content = ?, updatedAt = ? WHERE id = ? AND postId = ? """, (req.username, req.content, now, commentId, postId) ) await db.commit() async with db.execute("SELECT * FROM comments WHERE id = ? AND postId = ?", (commentId, postId)) as cursor: updated = await cursor.fetchone() return Comment(**dict(updated)) # 댓글 삭제 @app.delete("/api/posts/{postId}/comments/{commentId}", status_code=status.HTTP_204_NO_CONTENT) async def delete_comment(postId: str, commentId: str): async with aiosqlite.connect(DB_PATH) as db: async with db.execute("SELECT * FROM comments WHERE id = ? AND postId = ?", (commentId, postId)) as cursor: row = await cursor.fetchone() if not row: raise HTTPException(status_code=404, detail="댓글을 찾을 수 없음") await db.execute("DELETE FROM comments WHERE id = ? AND postId = ?", (commentId, postId)) await db.execute("UPDATE posts SET comments = comments - 1 WHERE id = ? AND comments > 0", (postId,)) await db.commit() return None # 게시물 좋아요 @app.post("/api/posts/{postId}/likes", status_code=status.HTTP_201_CREATED) async def like_post(postId: str, req: LikeRequest): async with aiosqlite.connect(DB_PATH) as db: post = await db.execute_fetchone("SELECT id FROM posts WHERE id = ?", (postId,)) if not post: raise HTTPException(status_code=404, detail="게시물을 찾을 수 없음") # 이미 좋아요 했는지 확인 exists = await db.execute_fetchone("SELECT 1 FROM likes WHERE postId = ? AND username = ?", (postId, req.username)) if exists: raise HTTPException(status_code=400, detail="이미 좋아요를 눌렀습니다.") await db.execute("INSERT INTO likes (postId, username) VALUES (?, ?)", (postId, req.username)) await db.execute("UPDATE posts SET likes = likes + 1 WHERE id = ?", (postId,)) await db.commit() return {"message": "좋아요 성공"} # 게시물 좋아요 취소 @app.delete("/api/posts/{postId}/likes", status_code=status.HTTP_204_NO_CONTENT) async def unlike_post(postId: str, username: str): async with aiosqlite.connect(DB_PATH) as db: post = await db.execute_fetchone("SELECT id FROM posts WHERE id = ?", (postId,)) if not post: raise HTTPException(status_code=404, detail="게시물을 찾을 수 없음") exists = await db.execute_fetchone("SELECT 1 FROM likes WHERE postId = ? AND username = ?", (postId, username)) if not exists: raise HTTPException(status_code=404, detail="좋아요를 찾을 수 없음") await db.execute("DELETE FROM likes WHERE postId = ? AND username = ?", (postId, username)) await db.execute("UPDATE posts SET likes = likes - 1 WHERE id = ? AND likes > 0", (postId,)) await db.commit() return None # CORS: 모든 도메인 허용 app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # --- Swagger UI 및 OpenAPI 문서 커스텀 엔드포인트 --- OPENAPI_YAML_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "openapi.yaml")) @app.get("/openapi.yaml", include_in_schema=False) async def get_openapi_yaml(): return FileResponse(OPENAPI_YAML_PATH, media_type="text/yaml") @app.get("/", include_in_schema=False) async def swagger_ui(): # Swagger UI HTML을 커스텀하여 서버 주소를 동적으로 현재 origin + '/api'로 설정 html = """ Simple Social Media API - Swagger UI
""" return HTMLResponse(content=html, status_code=200) @app.on_event("startup") async def on_startup(): await init_db() # 게시물 목록 조회 @app.get("/api/posts", response_model=list[Post]) async def list_posts(): async with aiosqlite.connect(DB_PATH) as db: db.row_factory = aiosqlite.Row rows = await db.execute_fetchall("SELECT * FROM posts ORDER BY createdAt DESC") return [Post(**dict(row)) for row in rows] # 게시물 생성 @app.post("/api/posts", response_model=Post, status_code=status.HTTP_201_CREATED) async def create_post(req: PostCreateRequest): import os print(f"[DEBUG][create_post] DB_PATH: {DB_PATH}, exists: {os.path.exists(DB_PATH)}", flush=True) post_id = str(uuid.uuid4()) now = now_iso() post = Post( id=post_id, username=req.username, content=req.content, createdAt=now, updatedAt=now, likes=0, comments=0 ) async with aiosqlite.connect(DB_PATH) as db: await db.execute( """ INSERT INTO posts (id, username, content, createdAt, updatedAt, likes, comments) VALUES (?, ?, ?, ?, ?, 0, 0) """, (post.id, post.username, post.content, post.createdAt, post.updatedAt) ) await db.commit() return post # 단일 게시물 조회 @app.get("/api/posts/{postId}", response_model=Post) async def get_post(postId: str = Path(...)): async with aiosqlite.connect(DB_PATH) as db: db.row_factory = aiosqlite.Row async with db.execute("SELECT * FROM posts WHERE id = ?", (postId,)) as cursor: row = await cursor.fetchone() if not row: raise HTTPException(status_code=404, detail="게시물을 찾을 수 없음") print(f"[DEBUG][get_post] DB_PATH: {DB_PATH}, exists: {os.path.exists(DB_PATH)}", flush=True) print(f"[DEBUG][get_post] SELECT * FROM posts WHERE id = {postId}", flush=True) print(f"[DEBUG][get_post] row: {row}", flush=True) data = dict(row) required_fields = {"id", "username", "content", "createdAt", "updatedAt", "likes", "comments"} missing = required_fields - data.keys() if missing: raise HTTPException(status_code=500, detail=f"DB 데이터에 필수 필드 누락: {missing}") try: return Post(**data) except Exception as e: raise HTTPException(status_code=500, detail=f"Post 모델 변환 오류: {e}") # 게시물 업데이트 @app.patch("/api/posts/{postId}", response_model=Post) async def update_post(postId: str, req: PostUpdateRequest): now = now_iso() async with aiosqlite.connect(DB_PATH) as db: db.row_factory = aiosqlite.Row async with db.execute("SELECT * FROM posts WHERE id = ?", (postId,)) as cursor: row = await cursor.fetchone() if not row: raise HTTPException(status_code=404, detail="게시물을 찾을 수 없음") await db.execute( """ UPDATE posts SET username = ?, content = ?, updatedAt = ? WHERE id = ? """, (req.username, req.content, now, postId) ) await db.commit() async with db.execute("SELECT * FROM posts WHERE id = ?", (postId,)) as cursor: updated = await cursor.fetchone() return Post(**dict(updated)) # 게시물 삭제 @app.delete("/api/posts/{postId}", status_code=status.HTTP_204_NO_CONTENT) async def delete_post(postId: str): async with aiosqlite.connect(DB_PATH) as db: async with db.execute("SELECT * FROM posts WHERE id = ?", (postId,)) as cursor: row = await cursor.fetchone() if not row: raise HTTPException(status_code=404, detail="게시물을 찾을 수 없음") await db.execute("DELETE FROM posts WHERE id = ?", (postId,)) await db.commit() return None # ISO8601 타임스탬프 생성 함수 from datetime import datetime, timezone def now_iso() -> str: return datetime.now(timezone.utc).isoformat() if __name__ == "__main__": uvicorn.run("main:app", host="0.0.0.0", port=8080, reload=True)