* 해당 프로젝트는 점프 투 FastAPI의 저자 추천 추가 기능에 해당하는 내용으로
자세한 프로젝트의 내용이나 이전 프로젝트의 내용은 위키독스를 참조해 주시기 바랍니다.
FastAPI & Svelte - 댓글
댓글 모델
# models.py
...
class Comment(Base):
__tablename__ = "comment"
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="comments")
answer_id = Column(Integer, ForeignKey("answer.id"))
answer = relationship("Answer", backref="comments")
user_id = Column(Integer, ForeignKey("user.id"), nullable=True)
user = relationship("User", backref="comment_users")
modify_date = Column(DateTime, nullable=True)
댓글은 질문, 답변 둘 다 댓글을 달 수 있도록 설정.
모델을 변경하였으므로 DB 파일 갱신.
(myapi) myapi> alembic revision --autogenerate
...
(myapi) myapi> alembic upgrade head
댓글 API
댓글 API 명세
API명 | URL | 요청 방법 | 설명 |
질문 댓글 등록 | /api/comment/create/question/{quetion_id} | post | 질문 댓글을 등록한다. |
답변 댓글 등록 | /api/comment/create/answer/{answer_id} | post | 답변 댓글을 등록한다. |
댓글 조회 | /api/comment/detail | get | 댓글을 조회한다. |
댓글 수정 | /api/comment/update | put | 댓글을 수정한다. |
댓글 삭제 | /api/comment/delete | delete | 댓글을 삭제한다. |
질문 댓글 등록 API
입력 항목
content
: 등록할 댓글의 내용
출력 항목
- 없음
답변 댓글 등록 API
입력 항목
content
: 등록할 댓글의 내용
출력 항목
- 없음
댓글 조회 API
입력 항목
comment_id
: 조회할 댓글의 고유번호
출력 항목
- Comment 스키마
댓글 수정 API
입력 항목
comment_id
: 수정할 댓글의 고유번호content
: 수정할 댓글의 내용
출력 항목
- 없음
댓글 삭제 API
입력 항목
comment_id
: 삭제할 댓글의 고유번호
출력 항목
- 없음
기존 User, Question, Answer을 처리했던 것과 같이 domain 폴더 내에 comment 폴더를 만들어 처리한다.
스키마
# domain/comment/comment_schema.py
import datetime
from pydantic import BaseModel, field_validator
from domain.user.user_schema import User
from typing import Union
class CommentCreate(BaseModel):
content: str
@field_validator('content')
def not_empty(cls, v):
if not v or not v.strip():
raise ValueError('빈 값은 허용되지 않습니다.')
return v
class Comment(BaseModel):
id: int
content: str
create_date: datetime.datetime
user: Union[User, None]
question_id: int
answer_id: int
modify_date: Union[datetime.datetime, None]
class CommentList(BaseModel):
total: int = 0
comment_list: list[Comment] = []
class CommentUpdate(CommentCreate):
comment_id: int
class CommentDelete(BaseModel):
comment_id: int
기존 스키마를 작성했던 방식과 동일하게 작성.
Python 3.10 버전 미만에서는 ‘|’
연산자를 인식하지 않기 때문에 Union, Optional 등을 이용해 처리해주면 된다.
...
from typing import Union, Optional
#Union 사용 예시
class Comment(BaseModel):
...
user: Union[User, None]
...
modify_date: Union[datetime.datetime, None]
# Optional 사용 예시
class Comment(BaseModel):
...
user: Optional[User] = None
...
modify_date: Optional[datetime.datetime] = None
질문, 답변에 작성된 댓글을 호출해야 하므로 질문, 답변 스키마에 comments 리스트를 추가한다.
# domain/question/question_schema.py
...
from domain.comment.comment_schema import Comment
...
class Question(BaseModel):
id: int
subject: str
content: str
create_date: datetime.datetime
answers: list[Answer] = []
user: Union[User, None]
modify_date: Union[datetime.datetime, None]
voter: list[User] = []
#edit
comments: list[Comment] = []
...
CRUD
# domain/comment/comment_curd.py
from datetime import datetime
from sqlalchemy.orm import Session
from domain.comment.comment_schema import CommentCreate, CommentUpdate
from models import Question, Answer, User, Comment
def create_comment_question(db: Session, question: Question,
comment_create: CommentCreate, user: User):
db_comment = Comment(question=question,
content=comment_create.content,
create_date=datetime.now(),
user=user)
db.add(db_comment)
db.commit()
def create_comment_answer(db: Session, answer: Answer,
comment_create: CommentCreate, user: User):
db_comment = Comment(answer=answer,
content=comment_create.content,
create_date=datetime.now(),
user=user
)
db.add(db_comment)
db.commit()
def get_comment(db: Session, comment_id: int):
comment = db.query(Comment).get(comment_id)
return comment
def update_comment(db: Session, db_comment: Comment,
comment_update: CommentUpdate):
db_comment.content = comment_update.content
db_comment.modify_date = datetime.now()
db.add(db_comment)
db.commit()
def delete_comment(db: Session, db_comment: Comment):
db.delete(db_comment)
db.commit()
라우터
# domain/comment/comment_router.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from starlette import status
from database import get_db
from domain.answer import answer_schema, answer_crud
from domain.question import question_crud
from domain.comment import comment_schema, comment_crud
from domain.user.user_router import get_current_user
from models import User
router = APIRouter(
prefix="/api/comment",
)
@router.post("/create/question/{question_id}", status_code=status.HTTP_204_NO_CONTENT)
def comment_create_question(question_id: int,
_comment_create: comment_schema.CommentCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)):
question = question_crud.get_question(db, question_id=question_id)
if not question:
raise HTTPException(status_code=404, detail="Question not found")
comment_crud.create_comment_question(db, question=question,
comment_create=_comment_create,
user=current_user)
@router.post("/create/answer/{answer_id}", status_code=status.HTTP_204_NO_CONTENT)
def comment_create_answer(answer_id: int,
_comment_create: comment_schema.CommentCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)):
answer = answer_crud.get_answer(db, answer_id=answer_id)
if not answer:
raise HTTPException(status_code=404, detail="Answer not found")
comment_crud.create_comment_answer(db, answer=answer,
comment_create=_comment_create,
user=current_user)
@router.get("/detail/{comment_id}", response_model=comment_schema.Comment)
def comment_detail(comment_id: int, db: Session = Depends(get_db)):
comment = comment_crud.get_comment(db, comment_id=comment_id)
return comment
@router.put("/update", status_code=status.HTTP_204_NO_CONTENT)
def comment_update(_comment_update: comment_schema.CommentUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)):
db_comment = comment_crud.get_comment(db, comment_id=_comment_update.comment_id)
if not db_comment:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
detail="데이터를 찾을수 없습니다.")
if current_user.id != db_comment.user.id:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
detail="수정 권한이 없습니다.")
comment_crud.update_comment(db=db, db_comment=db_comment,
comment_update=_comment_update)
@router.delete("/delete", status_code=status.HTTP_204_NO_CONTENT)
def comment_delete(_comment_delete: comment_schema.CommentDelete,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)):
db_comment = comment_crud.get_comment(db, comment_id=_comment_delete.comment_id)
if not db_comment:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
detail="데이터를 찾을수 없습니다.")
if current_user.id != db_comment.user.id:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
detail="삭제 권한이 없습니다.")
comment_crud.delete_comment(db=db, db_comment=db_comment)
질문 댓글 생성, 답변 댓글 생성, 댓글 조회, 수정, 삭제 API등을 생성했다.
기존 question, answer 라우터와 큰 차이가 없으므로 해당 코드를 이용해 수정한다.
api를 호출하기 위해 라우터 객체를 FastAPI에 등록해주어야 한다.
# main.py
...
from domain.question import question_router
from domain.answer import answer_router
from domain.user import user_router
from domain.comment import comment_router
...
app.include_router(question_router.router)
app.include_router(answer_router.router)
app.include_router(user_router.router)
app.include_router(comment_router.router)
app.mount("/assets", StaticFiles(directory="frontend/dist/assets"))
@app.get("/")
def index():
return FileResponse("frontend/dist/index.html")
댓글 목록 화면
댓글 목록은 별도 페이지가 아닌 질문 상세 페이지 → 질문, 답변 내용에 댓글 버튼을 클릭하여 보여주거나 그냥 표시하는 방식으로 처리할 수 있다.
댓글의 삭제 및 수정도 해당 페이지에서 한번에 처리할 수 있도록 하는 것이 사용자에게 편리하다.
질문 상세 페이지 - script
// Detail.svelte
<script>
...
export let params = {}
let question_id = params.question_id
let question = {answers:[], voter:[], content:'', comments: []}
let content = ""
let error = {detail: []}
let comment_content = ''
let show_comment_input = false
const toggle_comment = () => show_a_comment_input = !show_a_comment_input;
let comment_modify_id = -1
let answer_list = []
let size = 5
let total = 0
let sortBy = 'create_date'
let des = true
$: total_page = Math.ceil(total/size)
...
</script>
먼저 변수 및 상수를 선언하는 부분부터 살펴보면
let comment_content = ''
: 사용자의 댓글 입력 값을 받는 부분let show_comment_input = false
: 토글을 처리할 때 사용let comment_modify_id = -1
: 수정할 댓글의 ID 값을 받음.
이를 이용해 아래 함수를 만들면 된다.
// Detail.svelte
<script>
...
function post_comment_question() {
let url = "/api/comment/create/question/" + question_id
let params = {
content: comment_content
}
fastapi("post", url, params, (json) => {
comment_content = ''
error = {detail:[]}
get_question()
toggle_comment_input()
},
(err_json) => {
error = err_json
})
}
function toggle_comment_input() {
show_comment_input = show_comment_input
comment_content = ''
}
function update_comment(_comment_id, _comment_content) {
let url = '/api/comment/update'
let params = {
comment_id: _comment_id,
content: _comment_content
}
fastapi('put', url, params, (json) =>{
get_question()
stop_editing()
},
(err_json) => {
error = err_json
})
}
function delete_comment(_comment_id) {
if(window.confirm('정말로 삭제하시겠습니까?')) {
let url = "/api/comment/delete"
let params = {
comment_id: _comment_id
}
fastapi('delete', url, params, (json) => {
get_question()
},
(err_json) => {
error = err_json
})
}
}
function toggle_comments() {
show_comment_input = !show_comment_input
comment_content = ''
comment_modify_id = -1
}
function start_editing(index, _content) {
comment_modify_id = index
comment_content = _content
show_comment_input = true
}
function stop_editing() {
comment_modify_id = -1
}
//
...
</script>
post_comment_question()
: 질문에 대한 댓글을 작성하는 API를 호출.update_comment()
: 작성한 댓글을 수정하는 API를 호출.delete_comment()
: 작성한 댓글을 삭제하는 API를 호출.toggle_comments()
: 댓글 토글. 버튼을 이용하여 토글로 처리할 예정이다.start_editing()
: 수정 화면을 호출하는 함수stop_editing()
: 수정 화면을 취소하고 기존 수정 중인 댓글의 고유 번호를 -1로 설정.
토글을 처리하는 방법은 toggle_comments()
함수처럼 처리할 수 있고, 위에서 const로 선언한 show_comment_input
처럼 처리하는 방법이 있는데 둘 다 정상 동작하므로 골라서 사용하면 된다.
질문 상세 페이지 - template
//Detail.svelte
<script>
...
</script>
<div>
...
{/if}
</div>
<!-- 댓글 토글 -->
<div class="my-3">
<Error error={error} />
<button class="btn btn-sm btn-outline-primary mb-3"
on:click ={() => toggle_comments()}
disabled = {!$is_login }>
댓글</button>
</div>
<!-- 토글 끝 -->
<!-- 댓글 목록 -->
{#if show_comment_input && question.comments.length > 0}
<div class="card my-3">
{#each question.comments as comment (comment.id)}
<div class="card-body">
{#if comment_modify_id === comment.id}
<!-- editIndex? -->
<div>
<input class="form-control mb-3" bind:value={comment.content}
disabled="{!$is_login}"
maxlength="500"
placeholder="Edit comment">
<button class="btn btn-sm btn-outline-secondary mb-3"
on:click={() => update_comment(comment.id, comment.content)}>저장</button>
<button class="btn btn-sm btn-outline-secondary mb-3"
on:click={() => stop_editing()}>취소</button>
</div>
{/if}
{#if comment_modify_id !== comment.id}
<div>
<div class="card-text" style="font-size:0.87rem">{@html marked.parse(comment.content)}</div>
<span class="text-muted fw-bold" style="font-size:0.8rem" >{comment.user.username}, </span>
{#if comment.modify_date}
<span class="text-muted" style="font-size:.7rem"> {moment(comment.modify_date).format("YYYY년 MM월 DD일 hh:mm a")}(수정됨) </span>
{:else}
<span class="text-muted" style="font-size:.7rem"> {moment(comment.create_date).format("YYYY년 MM월 DD일 hh:mm a")} </span>
{/if}
{#if comment.user && $username === comment.user.username}
<button on:click={()=> start_editing(comment.id, comment.comment_content)}
class="btn btn-outline-secondary mb-2"
style="--bs-btn-padding-y: .25rem; --bs-btn-padding-x: .5rem; --bs-btn-font-size: .6rem;">
수정</button>
<button on:click={() => delete_comment(comment.id)}
class="btn btn-outline-danger mb-2"
style="--bs-btn-padding-y: .25rem; --bs-btn-padding-x: .5rem; --bs-btn-font-size: .6rem;">
삭제</button>
{/if}
</div>
{/if}
</div>
{/each}
</div>
{/if}
<!-- 댓글 목록 끝 -->
<!-- 댓글 입력 -->
{#if show_comment_input}
<div class="mb-3">
<textarea rows="2" bind:value={comment_content} class="form-control mb-3" />
<button class="btn btn-sm btn-outline-secondary mb-3 {$is_login ? '' : 'disabled'}"
on:click="{post_comment_question}"
>댓글 등록</button>
<button class="btn btn-sm btn-outline-secondary mb-3"
on:click={()=> toggle_comment_input()}>취소</button>
</div>
{/if}
<!-- 댓글 입력 끝 -->
</div>
</div>
<button class="btn btn-secondary" on:click="{() => {
push('/')
}}">목록으로</button>
주석 처리한 부분을 살펴보면 댓글 토글, 댓글 목록, 댓글 입력 부분으로 나누어 볼 수 있다.
댓글 토글
<!-- 댓글 토글 -->
<div class="my-3">
<Error error={error} />
<button class="btn btn-sm btn-outline-primary mb-3"
on:click ={() => toggle_comments()}
disabled = {!$is_login }>
댓글
</button>
</div>
<!-- 토글 끝 -->
버튼 클릭 시 댓글 목록과 댓글 입력 창을 출력하게 해준다. 버튼을 다시 클릭하면 다시 감춰지게 된다.
댓글 목록
<!-- 댓글 목록 -->
{#if show_comment_input && question.comments.length > 0}
<div class="card my-3">
{#each question.comments as comment (comment.id)}
<div class="card-body">
{#if comment_modify_id === comment.id}
<!-- editIndex? -->
<div>
<input class="form-control mb-3" bind:value={comment.content}
disabled="{!$is_login}"
maxlength="500"
placeholder="Edit comment">
<button class="btn btn-sm btn-outline-secondary mb-3"
on:click={() => update_comment(comment.id, comment.content)}>저장</button>
<button class="btn btn-sm btn-outline-secondary mb-3"
on:click={() => stop_editing()}>취소</button>
</div>
{/if}
{#if comment_modify_id !== comment.id}
<div>
<div class="card-text" style="font-size:0.87rem">
{@html marked.parse(comment.content)}
</div>
<span class="text-muted fw-bold" style="font-size:0.8rem">
{comment.user.username}, </span>
{#if comment.modify_date}
<span class="text-muted" style="font-size:.7rem"> {moment(comment.modify_date).format("YYYY년 MM월 DD일 hh:mm a")}(수정됨) </span>
{:else}
<span class="text-muted" style="font-size:.7rem"> {moment(comment.create_date).format("YYYY년 MM월 DD일 hh:mm a")} </span>
{/if}
{#if comment.user && $username === comment.user.username}
<button on:click={()=> start_editing(comment.id, comment.comment_content)}
class="btn btn-outline-secondary mb-2"
style="--bs-btn-padding-y: .25rem; --bs-btn-padding-x: .5rem; --bs-btn-font-size: .6rem;">
수정</button>
<button on:click={() => delete_comment(comment.id)}
class="btn btn-outline-danger mb-2"
style="--bs-btn-padding-y: .25rem; --bs-btn-padding-x: .5rem; --bs-btn-font-size: .6rem;">
삭제</button>
{/if}
</div>
{/if}
</div>
{/each}
</div>
{/if}
<!-- 댓글 목록 끝 -->
question.comments.length > 0
: 댓글이 하나도 없으면 표시하지 않는다.
{#each question.comments as comment (comment.id)}
: svelte의 #each
문은 반복문과 동일한 기능을 한다. #if
와 마찬가지로 끝나는 지점을 명시해주어야 한다.
해당 코드는 질문에 작성된 댓글의 정보를 불러오며 해당하는 질문 리스트들을 반복하여 출력하는 역할을 한다.
댓글 입력
<!-- 댓글 입력 -->
{#if show_comment_input}
<div class="mb-3">
<textarea rows="2" bind:value={comment_content} class="form-control mb-3" />
<button class="btn btn-sm btn-outline-secondary mb-3 {$is_login ? '' : 'disabled'}"
on:click="{post_comment_question}"
>댓글 등록</button>
<button class="btn btn-sm btn-outline-secondary mb-3"
on:click={()=> toggle_comment_input()}>취소</button>
</div>
{/if}
<!-- 댓글 입력 끝 -->
댓글을 등록하는 화면 부분.
댓글 등록 버튼 클릭 시 post_comment_question
함수를 실행하여 댓글을 등록하는 API를 호출한다.
댓글 취소 버튼 클릭 시 작성 중이던 댓글 창을 닫는다.
답변 댓글
❗문제점
위의 question을 만들었던 방법으로 작성하여 실행해도 정상적으로 댓글의 기능들은 동작한다.
하지만 답변 댓글의 경우는 다르다. detail.svelte에 해당 코드를 전부 작성하게 되면 textarea 또는 <input>
과 같은 입력 폼에서 데이터가 bind 될 때 입력 폼이 개별 동작하지 않고 모두 데이터가 입력되는 문제점이 있다.
이는 bind:value
가 해당하는 폼에 데이터를 모두 반영하기 때문에 발생하는 문제점으로 이를 해결하기 위해서는 Component로 만들어 처리하면 문제를 해결할 수 있다.
1. Components → CommentAnswer.svelte
(답변에 대한 댓글을 위한 컴포넌트)
2. CommentAnswer.svelte
를 다음과 같이 수정
// frontend/src/components/CommentAnswer.svelte
<script>
import fastapi from "../lib/api"
import Error from "../components/Error.svelte"
import CommentForm from "./CommentForm.svelte";
import { link, push } from 'svelte-spa-router'
import { answer_page, is_login, sort_by, username, desc } from "../lib/store"
import moment from "moment/min/moment-with-locales"
import { marked } from "marked";
moment.locale('ko')
export let params = {}
export let question_id = params.question_id
let question = {answers:[], voter:[], content:'', comments:[]}
let error = {detail: []}
let answer_list = []
export let answer;
export let content = ''
export let comment_modify_id = -1
export let comment_input = false
let size = 5
let total = 0
function get_question() {
fastapi("get", "/api/question/detail/" + question_id, {}, (json) => {
question = json
})
}
function get_answer_list() {
let url = '/api/answer/list'
let params ={
question_id: question_id,
page: $answer_page,
size: size,
sort_by: $sort_by,
desc: $desc
}
fastapi('get', url, params, (json) => {
answer_list = json.answer_list
total = json.total
},
(err_json) => (
error = err_json
))
}
function post_comment_answer(_answer_id) {
let url = '/api/comment/create/answer/' + _answer_id
let params = {
content: content
}
fastapi('post', url, params, (json) => {
content = ''
toggle_comment()
get_answer_list()
},
(err_json) => {
error = err_json
})
window.location.reload()
}
function update_comment(_comment_id,_comment_content) {
let url = "/api/comment/update"
let params = {
comment_id: _comment_id,
content: _comment_content
}
fastapi('put', url, params, (json) => {
get_answer_list()
stop_editing()
},
(err_json) => {
error = err_json
})
}
function delete_comment(_comment_id,_answer_id) {
if(window.confirm('정말로 삭제하시겠습니까?')) {
let url = "/api/comment/delete"
let params = {
comment_id: _comment_id
}
fastapi('delete', url, params, (json) => {
get_question()
get_answer_list()
},
(err_json) => {
error = err_json
})
}
window.location.reload()
}
function toggle_comment() {
comment_input = !comment_input
content = ''
comment_modify_id = -1
}
function start_editing(index, _content) {
comment_modify_id = index
content = _content
comment_input = false
}
function stop_editing() {
comment_modify_id = -1
}
function vote_answer(_answer_id) {
if(window.confirm("추천 하시겠습니까?")) {
let url = "/api/answer/vote"
let params = {
answer_id: _answer_id
}
fastapi('post', url, params, (json) => {
get_question()
get_answer_list()
},
(err_json) => {
error = err_json
})
}
}
function delete_answer(_answer_id) {
if(window.confirm("정말 삭제하시겠습니까?")) {
let url = "/api/answer/delete"
let params = {
answer_id: _answer_id
}
fastapi('delete', url, params, (json) => {
push('/')
},
(err_json) => {
error = err_json
})
}
}
get_question()
$: $answer_page, $sort_by, $desc, get_answer_list()
</script>
<Error error={error} />
<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>
<!-- 댓글 목록 자리 -->
<!-- 댓글 목록 -->
{#if answer.comments.length > 0}
<div class="card my-3">
{#each answer.comments as comment (comment.id)}
<div class="card-body">
{#if comment_modify_id === comment.id}
<!-- editIndex? -->
<input class="form-control mb-3" bind:value={comment.content}
disabled="{!$is_login}"
maxlength="500"
placeholder="Edit comment">
<button class="btn btn-sm btn-outline-secondary mb-3"
on:click={() => update_comment(comment.id,comment.content)}>저장</button>
<button class="btn btn-sm btn-outline-secondary mb-3"
on:click={() => stop_editing()}>취소</button>
{/if}
{#if comment_modify_id !== comment.id}
<div>
<div class="card-text" style="font-size:0.87rem">{@html marked.parse(comment.content)}</div>
<span class="text-muted fw-bold" style="font-size:0.8rem" >{comment.user.username}, </span>
{#if comment.modify_date}
<span class="text-muted" style="font-size:.7rem"> {moment(comment.modify_date).format("YYYY년 MM월 DD일 hh:mm a")}(수정됨) </span>
{:else}
<span class="text-muted" style="font-size:.7rem"> {moment(comment.create_date).format("YYYY년 MM월 DD일 hh:mm a")} </span>
{/if}
{#if comment.user && $username === comment.user.username}
<button on:click={()=> start_editing(comment.id, comment.a_comment_content)}
class="btn btn-outline-secondary mb-1"
style="--bs-btn-padding-y: .17rem; --bs-btn-padding-x: .25rem; --bs-btn-font-size: .6rem;">
수정</button>
<button on:click={() => delete_comment(comment.id)}
class="btn btn-outline-danger mb-1"
style="--bs-btn-padding-y: .17rem; --bs-btn-padding-x: .25rem; --bs-btn-font-size: .6rem;">
삭제</button>
{/if}
</div>
{/if}
</div>
{/each}
</div>
{/if}
<div>
<Error error={error} />
<button class="btn btn-sm btn-outline-primary mb-3"
on:click={() => toggle_comment()}
disabled = "{!$is_login}"> 댓글 작성 </button>
</div>
{#if comment_input}
<div class="mb-3">
<input bind:value={content} class="form-control mb-3" />
<!-- <CommentForm bind:comment_content = {content}/> -->
<button class="btn btn-sm btn-outline-secondary mb-3 {$is_login ? '' : 'disabled'}"
on:click="{()=> post_comment_answer(answer.id)}"
>댓글 등록</button>
<button class="btn btn-sm btn-outline-secondary mb-3"
on:click={()=> toggle_comment()}>취소</button>
</div>
{/if}
</div>
</div>
작성한 컴포넌트를 다른 Svelte 파일에서 호출하기 위해서는 사용할 변수 및 상수를 export
선언하여 처리해야 한다.
이후 호출한 파일에서 export 선언한 변수를 매핑해준다.
3. Detail.svelte 수정
// Detail.svelte
<script>
...
import CommentAnswer from "../components/CommentAnswer.svelte"
...
</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_by = 'voter_count', $desc = true} }>추천순</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, idx (answer.id)}
<Error error={error} />
<CommentAnswer
question_id = {params.question_id}
content = {a_comment_content}
answer = {answer}
comment_modify_id = {comment_modify_id}
comment_input = {show_a_comment_input} />
{/each}
<!-- 페이징처리 시작 -->
...
</div>
스크립트 단 상단에 CommentAnswer
를 import 해준다. 이렇게 하면 CommentAnswer
에 선언한 함수를 가져올 수 있으므로 사용하지 않은 함수는 지워주면 된다.
이후 템플릿 단에서 export
선언한 변수들을 해당 페이지에서 선언한 변수와 매핑 시켜주면 된다.
question도 컴포넌트로 분리 할 수 있지만 해당 방법과 동일한 방식이므로 따로 작성하지는 않는다.
Component로 분리하여 작성하기 전 작성한 내용이 textarea나 input과 같은 부분에 반영되는 모습.
Component로 만든 후 따로 동작하는 모습.
결과
토글이 닫혀있는 경우
토글이 열린 경우
댓글 수정 클릭 시 작성된 내용이 bind되고 저장, 취소 버튼 생성
수정 완료된 모습
데이터 삭제 완료
에러가 발생하는 경우?
500 에러가 발생하는 경우는 대부분 코드 에러.
본인의 경우 500 에러 발생 이유는 int값이 제대로 전달되지 않는 에러가 있었는데, question_id와 answer_id를 제대로 불러오지 못하는 경우가 발생. 이러한 경우 스키마에 comment 추가 후 호출 시 리스트로 호출하여 사용. 이렇게 하면 get_question()
호출 시 자동으로 호출된다.
422 에러는 잘못된 타입의 인자가 전달된 경우 발생. 이 부분도 작성한 코드가 문제가 되는 경우가 대부분이므로 오타 또는 프론트엔드에서 작성한 코드가 잘못되진 않았는지 확인한다.
참조한 사이트
'Python > FastAPI' 카테고리의 다른 글
점프 투 FastAPI 추가 기능 - 답변 페이징 (0) | 2024.02.02 |
---|---|
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 |