Includes FastAPI+Jinja2+HTMX+SQLite implementation with seed categories, plus deployment templates. Co-authored-by: Cursor <cursoragent@cursor.com>
90 lines
3.3 KiB
Python
90 lines
3.3 KiB
Python
"""
|
|
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"),
|
|
)
|
|
|