Initial commit: add FastAPI MVP (모프) and existing web app

Includes FastAPI+Jinja2+HTMX+SQLite implementation with seed categories, plus deployment templates.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dsyoon
2026-02-16 17:17:22 +09:00
commit 27540269b7
37 changed files with 3246 additions and 0 deletions

89
models.py Normal file
View File

@@ -0,0 +1,89 @@
"""
models.py
요구사항의 테이블을 SQLAlchemy ORM 모델로 정의합니다.
테이블:
- users
- categories
- prompts
- likes
핵심 포인트:
- likes에는 (prompt_id, user_identifier) 유니크 제약을 걸어 "중복 좋아요"를 DB 레벨에서도 방지합니다.
- 프롬프트 검색은 title/content LIKE로 구현합니다(초경량 MVP 목적).
- 나중에 로그인(계정/세션)을 붙이기 쉽도록 users 테이블을 별도로 유지합니다.
"""
from __future__ import annotations
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Index, Integer, String, Text, UniqueConstraint, func
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
nickname: Mapped[str] = mapped_column(String(40), unique=True, index=True, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
class Category(Base):
__tablename__ = "categories"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(40), unique=True, nullable=False)
slug: Mapped[str] = mapped_column(String(60), unique=True, index=True, nullable=False)
prompts: Mapped[list["Prompt"]] = relationship(back_populates="category")
class Prompt(Base):
__tablename__ = "prompts"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
title: Mapped[str] = mapped_column(String(120), index=True, nullable=False)
content: Mapped[str] = mapped_column(Text, nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
category_id: Mapped[int] = mapped_column(ForeignKey("categories.id"), index=True, nullable=False)
# MVP에서는 로그인 없이 nickname 문자열로 작성자를 기록합니다.
author_nickname: Mapped[str] = mapped_column(String(40), index=True, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True, nullable=False)
copy_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
category: Mapped["Category"] = relationship(back_populates="prompts")
likes: Mapped[list["Like"]] = relationship(back_populates="prompt", cascade="all, delete-orphan")
__table_args__ = (
# 정렬/필터가 잦은 필드 위주로 인덱스(SQLite에서도 도움).
Index("ix_prompts_category_created", "category_id", "created_at"),
)
class Like(Base):
__tablename__ = "likes"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
prompt_id: Mapped[int] = mapped_column(ForeignKey("prompts.id"), index=True, nullable=False)
# 쿠키 UUID 또는 IP 기반 식별자를 해시한 값(개인정보 최소화)
user_identifier: Mapped[str] = mapped_column(String(64), index=True, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
prompt: Mapped["Prompt"] = relationship(back_populates="likes")
__table_args__ = (
UniqueConstraint("prompt_id", "user_identifier", name="uq_likes_prompt_user"),
)