FastAPI로 완벽한 블로그 구축: 태그별 필터링

Leapcell HQ·2025년 10월 13일
post-thumbnail

이전 게시물에서는 블로그에 대한 태그 생성 및 표시 기능을 추가했습니다.

다음으로 태그 기능의 나머지 부분인 태그별 게시물 필터링을 완료할 것입니다.

사용자가 태그 링크를 클릭하면 해당 특정 태그에 속한 게시물만 표시하는 새 페이지로 이동합니다. 이를 위해 백엔드에 새 라우트와 처리 로직, 그리고 프론트엔드에 해당 뷰를 생성해야 합니다.

단계 1: 서비스 로직 확장

먼저 tags_service.py에 두 개의 새 메서드를 추가해야 합니다. 하나는 태그 ID로 태그를 찾아(이름을 얻기 위해) 다른 하나는 태그 ID별로 관련된 모든 게시물을 찾는 것입니다.

tags_service.py 파일을 열고 다음 내용을 추가합니다.

# tags_service.py
import uuid
from typing import List
from sqlmodel import Session, select
from models import Tag, Post, PostTagLink # Post 및 PostTagLink 가져오기

def find_or_create_tags(tag_names: List[str], session: Session) -> List[Tag]:
    """
    태그 이름 목록을 기반으로 태그 엔티티를 찾거나 생성합니다.
    """
    # ... 이 함수는 변경되지 않습니다 ...
    tags = []
    if not tag_names:
        return tags

    statement = select(Tag).where(Tag.name.in_(tag_names))
    existing_tags = session.exec(statement).all()
    tags.extend(existing_tags)

    existing_tag_names = {tag.name for tag in existing_tags}

    new_tag_names = [name for name in tag_names if name not in existing_tag_names]

    for name in new_tag_names:
        new_tag = Tag(name=name)
        session.add(new_tag)
        tags.append(new_tag)

    session.commit()

    for tag in tags:
        if tag.id is None:
            session.refresh(tag)

    return tags

def get_tag_by_id(tag_id: uuid.UUID, session: Session) -> Tag | None:
    """ID로 단일 태그 찾기"""
    return session.get(Tag, tag_id)

def get_posts_by_tag_id(tag_id: uuid.UUID, session: Session) -> List[Post]:
    """태그 ID로 관련된 모든 게시물 찾기"""
    statement = (
        select(Post)
        .join(PostTagLink, Post.id == PostTagLink.post_id)
        .where(PostTagLink.tag_id == tag_id)
        .order_by(Post.createdAt.desc())
    )
    posts = session.exec(statement).all()
    return posts

코드 설명:

  • get_tag_by_id: 기본 키로 태그 객체를 가져오는 간단한 헬퍼 함수입니다.
  • get_posts_by_tag_id: 이것이 핵심 쿼리 로직입니다. SQLModel의 selectjoin 메서드를 사용하여 PostTagLink 연결 테이블을 통해 제공된 tag_id와 관련된 모든 Post 객체를 필터링하고 생성 시간 기준으로 내림차순으로 정렬합니다.

단계 2: 태그 라우트 생성

이제 /tags/{tag_id} 요청을 처리할 라우트를 구현해 보겠습니다.

먼저 routers 폴더 안에 tags.py라는 새 파일을 만듭니다.

# routers/tags.py
import uuid
from fastapi import APIRouter, Request, Depends
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from sqlmodel import Session

from database import get_session
from auth_dependencies import get_user_from_session
import tags_service
import tracking_service

router = APIRouter()
templates = Jinja2Templates(directory="templates")

@router.get("/tags/{tag_id}", response_class=HTMLResponse)
def get_posts_by_tag(
    request: Request,
    tag_id: uuid.UUID,
    session: Session = Depends(get_session),
    user: dict | None = Depends(get_user_from_session),
):
    # 1. 이 태그 아래의 모든 게시물 가져오기
    posts = tags_service.get_posts_by_tag_id(tag_id, session)
    
    # 2. 페이지에 태그 이름을 표시하기 위해 태그 정보 가져오기
    tag = tags_service.get_tag_by_id(tag_id, session)
    
    # 3. 홈페이지와 일관성을 유지하기 위해 게시물 목록의 조회 수를 일괄 가져오기
    post_ids = [post.id for post in posts]
    view_counts = tracking_service.get_counts_by_post_ids(post_ids, session)
    for post in posts:
        post.view_count = view_counts.get(post.id, 0)
    
    tag_name = tag.name if tag else "Unknown"

    return templates.TemplateResponse(
        "posts-by-tag.html",
        {
            "request": request,
            "posts": posts,
            "user": user,
            "filter_name": tag_name,
            "title": f"Posts in {tag_name}",
        },
    )

마지막으로 main.py에 이 새 라우터 모듈을 포함시키는 것을 잊지 마세요.

# main.py
# ... 다른 가져오기
from routers import posts, users, auth, comments, uploads, tags # tags 라우터 가져오기

# ...

# 라우터 포함
app.include_router(posts.router)
app.include_router(users.router)
app.include_router(auth.router)
app.include_router(comments.router)
app.include_router(uploads.router)
app.include_router(tags.router) # 태그 라우터 마운트

단계 3: 프론트엔드 뷰 생성

마지막 단계는 templates 폴더에 posts-by-tag.html 뷰 파일을 만드는 것입니다. 이 파일은 태그별로 필터링된 게시물 목록을 표시하는 데 사용되며 내용은 index.html과 매우 유사합니다.

templates/posts-by-tag.html 파일을 만듭니다.

{% include "_header.html" %}

<div class="filter-header">
  <h2>Posts in Tag: <strong>{{ filter_name }}</strong></h2>
</div>

{% if posts %}
<div class="post-list">
  {% for post in posts %}
  <article class="post-item">
    <h2><a href="/posts/{{ post.id }}">{{ post.title }}</a></h2>
    <p>{{ post.content[:150] }}...</p>
    <small>{{ post.createdAt.strftime('%Y-%m-%d') }} | Views: {{ post.view_count }}</small>
  </article>
  {% endfor %}
</div>
{% else %}
<p>No posts found in this tag.</p>
{% endif %}

<a href="/" class="back-link" style="margin-top: 2rem;">&larr; Back to Home</a>

{% include "_footer.html" %}

이 템플릿은 동적으로 제목(예: "Posts in Tag: Python")과 해당 태그 아래의 게시물 목록을 표시합니다. 태그 아래에 게시물이 없으면 해당 메시지가 표시됩니다.

실행 및 테스트

애플리케이션을 다시 시작합니다.

uvicorn main:app --reload

브라우저를 열고 태그가 있는 게시물로 이동한 다음 게시물 아래의 아무 태그 링크나 클릭합니다.

해당 태그의 필터 페이지로 리디렉션되며 해당 태그 아래의 모든 게시물 목록을 볼 수 있습니다.

ImageP1

이 두 튜토리얼을 통해 블로그에 완전한 태그 시스템을 추가했습니다.

이 시점에서 FastAPI 블로그 프로젝트는 기본 인프라부터 핵심 기능, 콘텐츠 구성, 데이터 분석까지 모든 것을 다루었습니다.

블로그 기능의 잠재력은 무궁무진합니다. 현재 프레임워크를 기반으로 더 많은 기능을 계속 추가할 수 있습니다. 나머지는 여러분의 상상력에 달려 있습니다!

Leapcell에 배포하는 것을 잊지 마세요. FastAPI 지원, PostgreSQL 데이터베이스, Redis, 웹 분석 및 웹 애플리케이션 구축에 필요한 모든 도구를 제공합니다.

Leapcell


X에서 저희를 팔로우하세요: @LeapcellKR


블로그에서 읽기

관련 글:

0개의 댓글