3-16 도전! 저자 추천 파이보 추가 기능
이 책에서 구현할 파이보의 기능은 아쉽지만 여기까지이다. 함께 더 많은 기능을 추가하고 싶지만 이 책은 파이보의 완성이 아니라 파이보를 성장시키며 얻게 되는 경험을 전달하는 것을…
wikidocs.net
* 해당 내용은 위키독스의 저자 추천 추가 기능에 해당하는 내용입니다.
이전 또는 자세한 프로젝트의 내용은 위키독스를 참조해주시기 바랍니다.
FastAPI & Svelte - 답변 페이징
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
}
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>
스크립트 단에서 변경점은 다음과 같다.
- 스토어 변수로 선언했던 변수들(answer_page, sort_by, desc)을 import하여 가져온다.
- 답변 리스트 갱신 시 정렬 조건과 정렬 방식(sort_by, desc)을 파라미터로 전달한다.
- 반응형 변수로 선언하여 $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’
로 설정한다.
출력 결과
추천 순 정렬시 추천이 가장 많은 답변이 위로 가도록 표시된다.
최신 순 정렬
오래된 순 정렬
'Python > FastAPI' 카테고리의 다른 글
점프 투 FastAPI 추가 기능 - 댓글 (1) | 2024.02.05 |
---|---|
Svelte - Store(스토어) (0) | 2024.01.30 |
FastAPI & Svelte - 질문 목록 화면 (0) | 2024.01.27 |
FastAPI & Svelte - 질문 목록 API (1) | 2024.01.26 |
FastAPI & Svelte - 프로젝트 기초 진행하기 (0) | 2024.01.24 |