본문 바로가기
Python/FastAPI

점프 투 FastAPI 추가 기능 - 댓글

by 깐테 2024. 2. 5.

https://wikidocs.net/177232

 

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

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

wikidocs.net

 

* 해당 프로젝트는 점프 투 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>
  1. post_comment_question() : 질문에 대한 댓글을 작성하는 API를 호출.
  2. update_comment(): 작성한 댓글을 수정하는 API를 호출.
  3. delete_comment(): 작성한 댓글을 삭제하는 API를 호출.
  4. toggle_comments(): 댓글 토글. 버튼을 이용하여 토글로 처리할 예정이다.
  5. start_editing(): 수정 화면을 호출하는 함수
  6. 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 에러는 잘못된 타입의 인자가 전달된 경우 발생. 이 부분도 작성한 코드가 문제가 되는 경우가 대부분이므로 오타 또는 프론트엔드에서 작성한 코드가 잘못되진 않았는지 확인한다.

참조한 사이트

[Svelte] Props

 

[Svelte] Props

상위 컴포넌트에서 하위 컴포넌트로 전달되는 데이터들을 Props라고 합니다. Svelte의 Props을 이야기 합니다.

beomy.github.io

 

반응형