본문 바로가기
Python/FastAPI

점프 투 FastAPI 추가 기능 - 답변 페이징

by 깐테 2024. 2. 2.

https://wikidocs.net/177232

 

3-16 도전! 저자 추천 파이보 추가 기능

이 책에서 구현할 파이보의 기능은 아쉽지만 여기까지이다. 함께 더 많은 기능을 추가하고 싶지만 이 책은 파이보의 완성이 아니라 파이보를 성장시키며 얻게 되는 경험을 전달하는 것을…

wikidocs.net

 

* 해당 내용은 위키독스의 저자 추천 추가 기능에 해당하는 내용입니다.

이전 또는 자세한 프로젝트의 내용은 위키독스를 참조해주시기 바랍니다.


FastAPI & Svelte - 답변 페이징

4-01 답변 페이징과 정렬

 

4-01 답변 페이징과 정렬

- `완성 소스` : [https://github.com/kjrstory/fastapi_vue/tree/v3.16.1](https://github.com/kjrstory/fast…

wikidocs.net

참조한 페이지.

 

답변 페이징

답변 목록 API 수정

입력 항목

  • page: 페이지 번호
  • size: 한 페이지에 보여줄 게시물 개수

출력 항목

  • total: 전체 답변 개수
  • answer_id: 답변 목록

 

답변 목록 CRUD

# domain/answer/answer_crud.py
...
def get_answer_list(db: Session, skip: int = 0, limit = 5):
    _answer_list = db.query(Answer)\
        .order_by(Answer.create_date.desc())

    total = _answer_list.count()
    answer_list = _answer_list.offset(skip).limit(limit).all()
    return total, answer_list # (전체 건수, 페이징 적용된 답변 목록)

skip: 데이터 시작 위치

limit: 시작 위치부터 가져올 데이터의 건수.

 

전체 건수인 total은 offset, limit를 적용하기 전에 구해야 한다.

 

답변 목록 스키마

# domain/answer/answer_schema.py

...
class AnswerList(BaseModel):
    total: int = 0
    answer_list: list[Answer] = []

 

답변 목록 라우터

# answer_router.py
...
@router.get("/list", response_model=answer_schema.AnswerList)
def answer_list(question_id: int, db: Session = Depends(get_db),
                page: int = 0, size: int = 5):
    question = question_crud.get_question(db, question_id=question_id)
    if not question:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
                            detail="질문을 찾을 수 없습니다.")
    total, _answer_list = answer_crud.get_answer_list(
        db, question_id = question_id, skip = page * size, limit=size
    )
    return {
        'total': total,
        'answer_list': _answer_list
    }

답변 목록의 경우 detail/{question_id} 형식의 url 속에서 답변 페이징을 처리해야 하므로

어떤 질문에 대한 답변 내용을 가져와야 할 지 알아야 하기 때문에 question_id 값을 필요로 한다.

 

추천 순 정렬을 위한 모델

# domain/models.py
...
class Answer(Base):
    __tablename__ = "answer"

    id = Column(Integer, primary_key=True)
    content = Column(Text, nullable=False)
    create_date = Column(DateTime, nullable=False)
    question_id = Column(Integer, ForeignKey("question.id"))
    question = relationship("Question", backref="answers")
    user_id = Column(Integer, ForeignKey("user.id"), nullable=True)
    user = relationship("User", backref="answer_users")
    modify_date = Column(DateTime, nullable=True)
    voter = relationship('User', secondary=answer_voter, backref='answer_voters')
    voter_count = Column(Integer, default = 0)
...

 

모델이 변경되었으므로 DB 업데이트 수행

(myapi) myapi> alembic revision --autogenerate
(myapi) myapi> alembic upgrade head

 

voter_count에 추천인이 반영되도록 수동으로 추가

(myapi) myapi> python
>>>from database import SessionLocal
>>>from models import Question, Answer
>>>from datetime import datetime
>>>db = SessionLocal()
>>>answers = db.query(Answer).all()
>>>for answer in answers:
    answer.voter_count = len(answer.voter)
>>>db.commit()

 

정렬 로직 구현

API 명세 수정

답변 목록 API 입력 항목

  • question_id: 답변의 질문 id
  • page: 답변 목록 페이지
  • size: 한 페이지에 보여줄 목록 크기
  • sort_by: 정렬 방법
  • desc: 오름/내림 차순 여부

 

CRUD

# answer_crud.py
...
# 답변목록
def get_answer_list(db: Session, question_id: int, skip: int = 0, limit = 5,
                    sort_by: str = 'create_date',
                    desc: bool = True,):
    sort_column = getattr(Answer, sort_by)
    if desc: 
        sort_column = sort_column.desc()
    else:
        sort_column = sort_column.asc()
    _answer_list = db.query(Answer).filter(Answer.question_id == question_id)\
        .order_by(sort_column)

    total = _answer_list.count()
    answer_list = _answer_list.offset(skip).limit(limit).all()
    return total, answer_list # (전체 건수, 페이징 적용된 답변 목록

 

getattr(Answer, sort_by)

Answer 모델 클래스에서 sort_by로 지정된 속성의 값을 동적으로 가져오는 코드. 기본 값은 parameter로 지정한 ‘create_date’를 기본 값으로 가져온다.

 

답변에 추천을 하게 되면 voter_count가 증가하게 해야 한다.

# answer_crud.py
...
def vote_answer(db: Session, db_answer: Answer, db_user: User):
    db_answer.voter.append(db_user)
    db_answer.voter_count = db_answer.voter_count + 1
    db.commit()

 

라우터

# answer_router.py
...
@router.get("/list", response_model=answer_schema.AnswerList)
def answer_list(question_id: int, db: Session = Depends(get_db),
                sort_by: str = 'create_date',
                desc: bool = True,
                page: int = 0, size: int = 5):
    question = question_crud.get_question(db, question_id=question_id)
    if not question:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
                            detail="질문을 찾을 수 없습니다.")
    total, _answer_list = answer_crud.get_answer_list(
        db, question_id = question_id,
        sort_by=sort_by, desc=desc ,
        skip = page * size, limit=size
    )
    return {
        'total': total,
        'answer_list': _answer_list
    }

127.0.0.1:8000/docs에서 API 테스트 가능

FastAPI의 docs에서 호출 시 다음과 같이 정상적으로 출력 되는 것을 확인

 

스토어

질문 목록의 페이지와 유사하게 답변 목록의 페이지, 정렬 방법, 오름/내림차순 기능들도 스토어 변수로 저장해야 새로고침을 하더라도 사용할 수 있다.

단, 질문 목록에 쓰였던 변수 명과 동일하게 사용하면 충돌을 일으키므로 변수 명을 바꿔 사용한다.

// frontend/src/lib/store.js

...
export const page = persist_storage("page", 0)
export const access_token = persist_storage("access_token", "")
export const username = persist_storage("username", "")
export const is_login = persist_storage("is_login", false)
export const keyword = persist_storage("keyword", "")
//edit
export const answer_page = persist_storage("answer_page", 0)
export const sort_by = persist_storage("sort_by", "create_date")
export const desc = persist_storage("desc", true)

 

질문 상세 화면 변경

템플릿 - script 단

<script>
    ...
        //1.
    import { is_login, username, keyword, answer_page, sort_by, desc } from "../lib/store"
    import { marked } from 'marked'
    import moment from 'moment/min/moment-with-locales'
    ...

    export let params = {}
    let question_id = params.question_id
    let question = {answers:[], voter:[], content:''}
    let content = ""
    let error = {detail: []}

    let answer_list = []
    let size = 3
    let total = 0

    $: total_page = Math.ceil(total/size)

    ...
        //2.
    function get_answer_list() {
        let params = {
            question_id: question_id,
            page: $answer_page,
            size: size,
            //keyword: $keyword,
            sort_by: $sort_by,
            desc: $desc
        }
        fastapi('get', '/api/answer/list', params, (json) => {
            answer_list = json.answer_list
            total = json.total
            //kw = $keyword
        },
        (err_json) =>{
            error = err_json
        })
    }

    ...

        //3.
    $: $answer_page, $keyword, $sort_by, $desc, get_answer_list()
</script>

스크립트 단에서 변경점은 다음과 같다.

  1. 스토어 변수로 선언했던 변수들(answer_page, sort_by, desc)을 import하여 가져온다.
  2. 답변 리스트 갱신 시 정렬 조건과 정렬 방식(sort_by, desc)을 파라미터로 전달한다.
  3. 반응형 변수로 선언하여 $answer_page, $desc, $sort_by의 값이 변경 될 때 get_answer_list() 함수를 실행한다.

 

Home.svelte에서 선언했던 방식과 같은 방식으로 선언했다.

 

템플릿 - 추천, 최신, 오래된 순 버튼

// Detail.svelte

<script>

...

</script>

<div>

...

<button class="btn btn-secondary" on:click="{() => {
        push('/')
    }}">목록으로</button>

    <!-- 답변 목록 -->
    <!-- <h5 class="border-bottom my-3 py-2">{question.answers.length}개의 답변이 있습니다.</h5> -->
    <div class="row">
        <div class="col-6">
          <h5 class="border-bottom my-3 py-2 mb-0">{total}개의 답변이 있습니다.</h5>
        </div>
        <div class="col-6 d-flex justify-content-end align-items-center">
            <button class="btn mr-2 btn-outline-primary {$sort_by=== 'voter_count' ? 'active' : ''}"
                on:click={() => {sort_voter(),console.log()} }>추천순</button>
            <button class="btn btn-outline-primary {$sort_by === 'create_date' && $desc === true ? 'active' : ''}"
                on:click={() => {$sort_by = 'create_date',$desc = true}}>최신순</button>
            <button class="btn btn-outline-primary {$sort_by === 'create_date' && $desc === false ? 'active' : ''}"
                on:click="{() => ($sort_by = 'create_date', $desc = false)}">오래된순</button>
        </div>
    </div>

        {#each answer_list as answer (answer.id)}
...

각 요소를 버튼으로 선언하여 추천 순, 최신 순, 오래된 순으로 정렬되도록 설정했다.

 

스크립트에서 반응형 변수로 선언했기 때문에 $sort_by === ‘voter_count’ 와 같이 선언된 값이 변경되면 자동으로 get_answer_list() 함수를 호출하며 정렬 조건과 방식을 갱신한다.

 


...
        {#each answer_list as answer (answer.id)}
    <div class="card my-3">
        <div class="card-body">
            <div class="card-text">{@html marked.parse(answer.content)}</div>
            <div class="d-flex justify-content-end">
                {#if answer.modify_date }
                <div class="badge bg-light text-dark p-2 text-start mx-3">
                    <div class="mb-2">(수정됨)</div>
                    <div>{moment(answer.modify_date).format("YYYY년 MM월 DD일 hh:mm a")}</div>
                </div>
                {/if}
                <div class="badge bg-light text-dark p-2 text-start">
                    <div class="mb-2">{answer.user ? answer.user.username : ""}</div>
                    <div>{moment(answer.create_date).format("YYYY년 MM월 DD일 hh:mm a")}</div>
                </div>
            </div>
            <div class="my-3">
                <button class="btn btn-sm btn-outline-secondary"
                    on:click="{() => vote_answer(answer.id)}">추천
                    <span class="badge rounded-pill bg-success">{answer.voter.length}</span>
                </button>
                {#if answer.user && $username === answer.user.username }
                <a use:link href="/answer-modify/{answer.id}" 
                    class="btn btn-sm btn-outline-secondary">수정</a>
                <button class="btn btn-sm btn-outline-secondary"
                    on:click={() => delete_answer(answer.id) }>삭제</button>
                {/if}
            </div>
        </div>
    </div>
    {/each}

그 아래에 답변이 Pagination 되도록 처리한다. 이 방식 또한 Home.svelte에서 사용했던 방식과 같은 방식으로 페이징 처리했다.

 

질문 목록 변경

<script>
    import { link } from 'svelte-spa-router'
    import { page, keyword, access_token, username, is_login, answer_page, sort_by, desc } from "../lib/store"
</script>

<!-- 네비게이션바 -->
<nav class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
    <div class="container-fluid">
        <a use:link class="navbar-brand" href="/" 
            on:click="{() => {$keyword = '', $page = 0, $answer_page = 0, $sort_by = 'create_date', $desc = true}}">FastAPI & Svelte</a>
        <button
            ...
</nav>

네비게이션 바에서 로고를 클릭했을 때 스토어 변수가 초기화되도록 설정.

만약 추천 순으로 먼저 보이도록 설정하고 싶다면 ‘create_date’ 부분을 ‘voter_count’로 설정한다.

 

출력 결과

추천 순

 

추천 순 정렬시 추천이 가장 많은 답변이 위로 가도록 표시된다.

 

 

최신 순 정렬

 

 

오래된 순 정렬

 

 

반응형