웹 개발

[django] AI_ENTRY 웹페이지 프로젝트 -(6)

행니짱 2023. 11. 2. 21:59

이번 포스팅에서는 블로그 글 수정, 삭제 기능을 구현해보도록 하겠다. 

그 전에 css로 페이지를 좀 그럴싸하게 꾸며보려고 한다. 

 

웹페이지 디자인은 gpt에 부탁했다.

진심 내 개인적인 사견으로는 프론트엔드는 몇몇 인재들 제외하곤 종말할것 같다. 

요즘 생성형  AI가 너무 잘나온다 진짜로 ... 결국에는 인간의 창의성이 관건이어서, 단순 코더들은 다 사라질듯 

ch99 > static > index.html 

@font-face {
    font-family: 'NanumSquareRoundEB';
    src: url('/static/fonts/NanumSquareRoundEB.ttf') format('truetype'); /* 폰트 파일 경로 및 형식 */
}

/* 전체 페이지 스타일 */
body {
    font-family: 'NanumSquareRoundEB';
    background-color: #f0f0f0;
    margin: 0;
    padding: 0;
}

/* 제목 스타일 */
h1 {
    text-align: center;
    background-color: #007bff;
    color: #fff;
    padding: 20px;
}

/* 테이블 스타일 */
table {
    width: 100%;
    border-collapse: collapse;
    margin: 20px 0;
}

table th {
    background-color: #333;
    color: #fff;
    text-align: left;
    padding: 10px;
}

table th, table td {
    border: 1px solid #ccc;
    padding: 10px;
}

/* "글쓰기" 버튼 스타일 */
.btn_post {
    text-align: center;
    margin: 20px 0;
}

.btn_post a {
    display: inline-block;
    padding: 10px 20px;
    background-color: #007bff;
    color: #fff;
    text-decoration: none;
    border-radius: 5px;
}

.btn_post a:hover {
    background-color: #0056b3;
}
ch99 > template > base.html 
: index.html이 base.html을 상속 받아서, 여기에 index.css를 연결해주었다. 

<!doctype html>
<html lang="ko">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <link rel="stylesheet" type="text/css" href="{% static '/css/index.css' %}">
 
    <title>소프트웨어야 놀자</title>
</head>

: 맨 처음에는 연결이 안되서 당황했는데, css파일은 <head></head> 안에 써주지 않은게 오류 발생의 원인이었다. 

: navbar.html에도 똑같이 navbar.css를 적용해준다. 

 역시 사람도 옷차림에 따라 달라진다고, 웹페이지도 옷을 입히니까 떼깔이 좀 산다. 아직 로그인, 로그아웃 부분 디자인 좀 고치고, 페이지네이션 부분도 해결해야겠지만, 그래도 디자인을 입히니까 좀 그럴싸해보이지 않는가?!

 

그리고 이전 포스팅에서 문제점이었던 첨부파일이 다운이 안되는 문제점을 해결했다. 

 

1. settings.py에서 미디어 파일에 대한 설정을 추가로 해준다. 

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

: 'MEDIA_URL'은 미디어 파일을 제공할 URL경로이고, 'MEDIA_ROOT'는 서버 파일시스템에서 미디어 파일을 저장하는 경로이다.

: 그리고 프로젝트 디렉토리 아래에 'media'디렉토리를 만들어준다. 왜냐하면, 여기에에 나중에 업로드한 파일들이 저장된다. 

-> 업로드를 하면 비어있던 디렉토리 안에 파일들이 들어 있는것을 확인해볼 수 있다. 

2. URL 패턴 설정 

urlpatterns = [
    path('admin/', admin.site.urls),
    path('board/', include('board.urls')),
    path('common/', include('common.urls')),
    path('', views.index, name='index'),
    ]  + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

: 'urlspy'파일에서 'MEDIA_URL'을 서빙하기 위한 URL 패턴 추가하기 

3. 폼 수정하기 

class BoardForm(forms.ModelForm):    
   
    class Meta:
        model = Board
        fields = ['title', 'content', 'attachment']
        widgets = {
            'title': forms.TextInput(attrs={
                'class': 'title',
                'placeholder': '제목을 입력하세요'}),
            'content': forms.Textarea(attrs={
                'placeholder': '내용을 입력하세요'}),
            'attachment': forms.FileInput()  # 파일 업로드 위젯 설정
        }

: enctype = "multipart/form-data" 속성을 가진 form을 만들기 위해서는 폼 클래스에서 FileInput 위젯을 사용해야 한다. 

4. HTML 수정하기 

    <form method="post" enctype="multipart/form-data">
        {% csrf_token %}
        {{ form.as_p }}
        <button type="submit">게시물 작성</button>

: 게시물을 작성하는 HTML 에서도 <form> 엘리먼트에 enctype="multipart/form-data" 속성 추가해주기 

: 이렇게 하면 위와 같이 media앱에 미디어 파일들이 잘 올라가는 것을 볼 수 있다.

 

지금부터는 게시글 삭제와 수정을 구현해보도록 하겠다. 

1. 수정 view 작성해주기 

from django.contrib.auth import get_user_model

#수정 페이지
User = get_user_model()

@login_required(login_url='common:login')
def modify(request, post_id):
    post = get_object_or_404(Board, pk=post_id)

    # 첫 번째 조건: 현재 로그인한 사용자가 게시물의 작성자인지 확인
    if request.user == post.writer:
        if request.method == 'POST':
            form = BoardForm(request.POST, instance=post)
            if form.is_valid():
                post = form.save(commit=False)
                post.modify_date = timezone.now()
                post.save()  # 수정된 게시물을 저장
                messages.success(request, '게시물이 성공적으로 수정되었습니다.')
                return redirect('board:detail', post_id=post.id)
        else:
            form = BoardForm(instance=post)
        context = {'form': form, 'post': post}
        return render(request, 'board/write.html', context)
    else:
        # 두 번째 조건: 현재 로그인한 사용자가 게시물의 작성자가 아니라면 권한 에러 메시지 표시
        messages.error(request, '수정권한이 없습니다.')
        return redirect('board:detail', post_id=post.id)

: get_user_model()을 사용하여 User만 수정할 수 있도록 한다. 

: 만약 User가 post.writer가 맞다면, 수정할 수 있는 form을 넘겨주고, 아니라면 수정권한이 없다는 메시지를 띄운다. 

2, 수정 HTML 작성해주기 
: 페이지에 수정 버튼을 넣어줘야 하므로, HTML 파일을 수정해준다. 

        <div class="modify">
            {% if request.user == post.writer %}
                <a href="{% url 'board:modify' post.id  %}" class="btn btn-sm btn-outline-secondary">수정</a>
                <a href="{% url 'board:delete' post.id  %}" class="btn btn-sm btn-outline-secondary">삭제</a>
            {% endif %}
        </div>

: 나중에 삭제 기능도 구현할 예정이라, 삭제 버튼도 미리 넣어두었다. 

3. URL 패턴 추가하기 

urlpatterns = [
    path('', views.index, name='index'),
    path('<int:post_id>/', views.detail, name='detail'),
    path('write/', views.write, name='write'),
    path('modify/<int:post_id>/', views.modify, name='modify'),
    path('delete/<int:post_id>/', views.delete, name='delete'),
]

: 버튼을 누르면 수정 view와 연결 될 수 있도록 url도 연결해준다. 삭제 기능도 구현할 예정이므로, 삭제 url도 만들어주었으니, 이제 삭제 view도 작성해준다. 

 

4. 삭제 veiw 작성해주기 

#삭제 페이지
@login_required(login_url='common:login')
def delete(request, post_id):
    post = get_object_or_404(Board, pk=post_id)
   
    if request.user != post.writer:
        messages.error(request, '삭제권한이 없습니다')
        return redirect('board:detail', post_id=post.id)
    post.delete()
    return redirect('board:index')

: 삭제 veiw도 수정과 동일하게 @login_required를 데코레이터로 사용한다. @login_required는 인증되지 않은 사용자가 뷰로 접근하는 것을 막아준다. 로그인되지 않은 경우, 이 view 자체에 접근하는 것을 거부한다. 즉, 로그인해야 이 view에 접근할 수 있다는 뜻!  여기도 동일하게 user = post.writer여야만 삭제가 가능하다. 

 

: 수정하기를 누르면 바로 수정하기 폼이 뜨고, 삭제 버튼이 누르면 바로 게시물이 삭제되는 것을 볼 수 있다. 

 

마지막으로 댓글 기능을 구현할 수 있도록 detail view를 수정해보도록 하겠다. 

 

1. 댓글을 작성할 수 있는 Comment 모델 만들기 

class Comment(models.Model):
  post = models.ForeignKey(Board, on_delete=models.CASCADE, null=True)
  body = models.TextField()
  writer = models.ForeignKey(User, on_delete=models.CASCADE)
  date = models.DateTimeField(default=timezone.now)

: Comment 모델에는 4개의 필드가 있다. post 필드는 model이 Board모델과 관련있다는 것을 보여준다. on_delete = models. CASCADE는 게시물이 삭제될 때 댓글 comment도 함께 삭제된다는것을 의미한다. 

: body필드는 댓글의 모델을 저장하고, writer 필드는 댓글을 작성한 사용자를 나타낸다. writer 필드는 User모델과 관련이 있는데, writer가 곧 User와 같기 때문이다. 

; 마지막으로 date 필드는 댓글이 작성된 날짜와 시간을 저장하는데, models.DateTImeField를 사용하고 이때 default를 timezone.now로 하면 현재 시간으로 자동 저장할 수 있다. 

 

이제 이 comment 모델을 detail 함수에 넣어주어야 한다. 

2. ch99 > board 앱 디렉토리 > views.py > detail 함수 수정

#더보기 페이지
@login_required(login_url='common:login')
def detail(request, post_id):
    post = get_object_or_404(Board, pk=post_id)
    comments = Comment.objects.filter(post = post_id)
    if request.method == "POST":
        comment = Comment()
        comment.post = post
        comment.body = request.POST['body']
        comment.date = timezone.now()
        comment.writer = request.user
        comment.save()
   
    return render(request, 'board/detail.html', {'post': post, 'comments': comments})

: 만약 POST 요청일 경우 Commnet() 모델을 불러와서댓글을 작성할 수 있다. 양식을 제출하는 경우에 이 코드가 실행된다고 이해하면 된다. 

3. HTML 파일 수정하기 

    <br><br>
    댓글 달기 :
    <ul>
        {% for comment in comments %}
          <li>{{ comment.body }}</li>
          <li>{{ comment.writer }}</li>
        {% empty %}
          <li>댓글이 없습니다.</li>
        {% endfor %}
      </ul>
     
      <form method="POST">
        {% csrf_token %}
        <input name="body" type="text" value="">
        <input class="btn btn-primary" type="submit" value="댓글 남기기">
      </form>
{% endblock %}

: HTML파일에 달린 댓글을 가져와서 보여줄 수 있도록 코드 작성하기 

: POST 요청 - > 모델을 불러온다. input한 내용이 body가 되어 모델의 comment.body가 된다. 

: 작성된 댓글을 보여줄 수도 있고, POST 요청으로 댓글을 작성하여 comment객체에 저장할 수도 있다. 

 

이렇게 댓글을 달면 댓글이 잘 달리는 것을 확인할 수 있다 ! 

 

그런데, <li> 태그로 다니까, 너무 댓글이 댓글 답지 않은 것 같다. 

기사에 나온 댓글처럼 구현을 하려면 javascrip 지식도 필요하더라. . . 

결국 웹은 javascript다 이건가 ?! 

 

이번 포스팅은 여기서 마무리 

마지막 포스팅이 2주 전이더라. 

원래 겨울에는 취약한 인간이라 요즘에는 하던일도 줄이고 줄이고 있다. 

그냥 만사가 피곤하고, 집에서 쉬고만 싶은 마음 . . . 

 

다음 포스팅에서는 잠깐 Flutter에 대해 다뤄보려고 한다. 

오늘 갑자기 앱 만들기에 또 꽂혀서 찾아보니까 Flutter가 재밌다던데 ~