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:
89
models.py
Normal file
89
models.py
Normal 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"),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user