Skip to content

Latest commit

 

History

History
611 lines (550 loc) · 17.7 KB

README.md

File metadata and controls

611 lines (550 loc) · 17.7 KB

blog_project

목차

  1. 요구사항
  2. 프로젝트 구조
  3. 데이터베이스 구조
  4. 기능명세서
  5. 화면설계
  6. 관련 이슈
  7. 회고

요구사항명세

  • 프로젝트 일정: 10월 26일(목) ~ 11월 7일(화)
  • 11월 8일 개별 발표
  • 기술 blog 만들기
  • 모놀리식 (DRF는 이 프로젝트에서 사용하지 않습니다.)
  • 데이터베이스 구조를 설계
  • 사용언어

Python

  • 프레임워크

Django, Bootstrap, Bootstrap_icon

  • 배포

PC 서버는 23.11.08 이후로 내립니다.


2. 회원가입 기능 구현 - 회원가입을 할 수 있는 페이지가 있어야합니다.
- 입력받는 값은 id, password입니다.
3. 로그인 기능 구현 - 로그인을 할 수 있는 페이지가 있어야합니다.
- 입력받는 값은 id, password입니다.
4. 게시글 작성 기능 구현 - 로그인을 한 유저만 해당 기능을 사용 할 수 있습니다.
- 게시글 제목과 내용을 작성 할 수 있는 페이지가 있어야합니다.
- 작성한 게시글이 저장되어 게시글 목록에 보여야 합니다.
- 사진 업로드가 가능하도록 합니다.
- 게시글 조회수가 올라갈 수 있도록 합니다.
5. 게시글 목록 기능 구현 - 모든 사용자들이 게시한 블로그 게시글들의 제목을 확인 할 수 있습니다.
6. 게시글 상세보기 기능 구현 - 게시글의 제목/내용을 보는 기능입니다.
7. 게시글 검색 기능 구현 - 주제와 태그에 따라 검색이 가능하게 합니다.
- 검색한 게시물은 시간순에 따라 정렬이 가능해야 합니다.
8. 게시글 수정 기능 구현 - 로그인을 한 유저만 해당 기능을 사용 할 수 있습니다.
- 본인의 게시글이 아니라면 수정이 불가능합니다.
- 게시글의 제목 또는 내용을 수정 하는 기능입니다.
- 게시글 제목과 내용을 수정 할 수 있는 페이지가 있어야합니다.
- 수정된 내용은 게시글 목록보기/상세보기에 반영되어야합니다.
9. 게시글 삭제 기능 구현 - 로그인을 한 유저만 해당 기능을 사용 할 수 있습니다.
- 본인의 게시글이 아니라면 수정이 불가능합니다.
- 게시글을 삭제하는 기능입니다.
- 삭제를 완료한 이후에 게시글 목록 화면으로 돌아갑니다.
- 삭제된 게시글은 게시글 목록보기/상세보기에서 접근이 불가능하며,
접근 시도 시 '존재하지 않는 게시글입니다' 라는 페이지를 보여줍니다.
10. 회원 관련 추가 기능(UI 직접 구현 필요) - 비밀번호 변경기능
- 프로필 수정
- 닉네임 추가
11. 댓글 기능(UI 직접 구현 필요) - 댓글 추가
- 댓글 삭제
- 대댓글
12. 부가 기능 - 번역 기능 (en, kr)

WBS

gantt
    dateFormat  YYYY-MM-DD
    title       Django 기술블로그 
    excludes    weekends
    %% (`excludes` accepts specific dates in YYYY-MM-DD format, days of the week ("sunday") or "weekends", but not the word "weekdays".)

    section �전체
    요구사항 분석            :done,    des1, 2023-10-26, 0d
    데이터베이스 설계         :done,  des2, 2023-10-26, 0d
    개발                   :done, des3, after des2, 7d
    프레젠테이션             :active, des4, after des3, 2d

    section  개발
    회원가입 기능 구현        :done,    dev1, after des2, 0d
    로그인 기능 구현           :done, 1d
    게시글 CRUD 구현        :done,crit, 2d
    게시글 검색 기능            :done,0d
    회원 관련 추가 기능        :done,1d
    댓글 기능                :done,1d
    대댓글                   :done,12h
    마크다운 기능            :done,12h
    부가 기능                :done, 3h
    메인페이지 및 테마 구현     :done,1d
Loading

프로젝트 구조

펼치기
tutorialproject
 ┣ accounts
 ┃ ┣ migrations
 ┃ ┃ ┣ 0001_initial.py
 ┃ ┃ ┣ 0002_alter_user_profile_image.py
 ┃ ┃ ┣ 0003_remove_user_username.py
 ┃ ┃ ┣ 0004_alter_user_ip_address.py
 ┃ ┃ ┣ 0005_user_username.py
 ┃ ┃ ┣ 0006_alter_user_email_alter_user_password_and_more.py
 ┃ ┃ ┣ 0007_alter_user_email_alter_user_last_login_and_more.py
 ┃ ┃ ┣ 0008_alter_user_profile_image.py
 ┃ ┃ ┗ __init__.py
 ┃ ┣ templates
 ┃ ┃ ┗ accounts
 ┃ ┃ ┃ ┣ base.html
 ┃ ┃ ┃ ┣ join.html
 ┃ ┃ ┃ ┣ login.html
 ┃ ┃ ┃ ┣ password_change.html
 ┃ ┃ ┃ ┣ password_change_done.html
 ┃ ┃ ┃ ┗ profile.html
 ┃ ┣ __init__.py
 ┃ ┣ admin.py
 ┃ ┣ apps.py
 ┃ ┣ forms.py
 ┃ ┣ models.py
 ┃ ┣ tests.py
 ┃ ┣ urls.py
 ┃ ┗ views.py
 ┣ blog
 ┃ ┣ migrations
 ┃ ┃ ┣ 0001_initial.py
 ┃ ┃ ┣ 0002_category_tag_rename_contents_post_content_and_more.py
 ┃ ┃ ┣ 0003_post_user.py
 ┃ ┃ ┣ 0004_remove_post_writer.py
 ┃ ┃ ┣ 0005_alter_comment_comment_reply.py
 ┃ ┃ ┣ 0006_alter_comment_comment_reply_alter_post_content.py
 ┃ ┃ ┗ __init__.py
 ┃ ┣ templates
 ┃ ┃ ┗ blog
 ┃ ┃ ┃ ┣ base.html
 ┃ ┃ ┃ ┣ chat.html
 ┃ ┃ ┃ ┣ comment_form.html
 ┃ ┃ ┃ ┣ post_confirm_delete.html
 ┃ ┃ ┃ ┣ post_detail.html
 ┃ ┃ ┃ ┣ post_form.html
 ┃ ┃ ┃ ┣ post_list.html
 ┃ ┃ ┃ ┗ recursive_comment.html
 ┃ ┣ __init__.py
 ┃ ┣ admin.py
 ┃ ┣ apps.py
 ┃ ┣ forms.py
 ┃ ┣ models.py
 ┃ ┣ tests.py
 ┃ ┣ urls.py
 ┃ ┗ views.py
 ┣ django_tuieditor
 ┃ ┣ static
 ┃ ┣ templates
 ┃ ┃ ┗ django_tuieditor
 ┃ ┃ ┃ ┣ editor.html
 ┃ ┃ ┃ ┣ static_viewer.html
 ┃ ┃ ┃ ┗ viewer.html
 ┃ ┣ templatetags
 ┃ ┃ ┣ __init__.py
 ┃ ┃ ┣ gfm.py
 ┃ ┃ ┗ render_widget.py
 ┃ ┣ __init__.py
 ┃ ┣ apps.py
 ┃ ┣ fields.py
 ┃ ┣ models.py
 ┃ ┗ widgets.py
 ┣ locale
 ┃ ┣ en
 ┃ ┃ ┗ LC_MESSAGES
 ┃ ┃ ┃ ┣ django.mo
 ┃ ┃ ┃ ┗ django.po
 ┃ ┗ ko
 ┃ ┃ ┗ LC_MESSAGES
 ┃ ┃ ┃ ┣ django.mo
 ┃ ┃ ┃ ┗ django.po
 ┣ main
 ┃ ┣ migrations
 ┃ ┃ ┗ __init__.py
 ┃ ┣ templates
 ┃ ┃ ┗ main
 ┃ ┃ ┃ ┗ index.html
 ┃ ┣ __init__.py
 ┃ ┣ admin.py
 ┃ ┣ apps.py
 ┃ ┣ models.py
 ┃ ┣ tests.py
 ┃ ┣ urls.py
 ┃ ┗ views.py
 ┣ media
 ┃ ┣ account
 ┃ ┃ ┗ 2023
 ┃ ┃ ┃ ┣ 10
 ┃ ┃ ┃ ┃ ┣ 27
 ┃ ┃ ┃ ┃ ┃ ┣ Untitled.png
 ┃ ┃ ┃ ┃ ┃ ┗ Untitled_TOFcrAA.png
 ┃ ┃ ┃ ┃ ┗ 31
 ┃ ┃ ┃ ┃ ┃ ┣ jiman.jpg
 ┃ ┃ ┃ ┃ ┃ ┣ 서명.jpg
 ┃ ┃ ┃ ┃ ┃ ┗ 서명_8DVZFz8.jpg
 ┃ ┃ ┃ ┗ 11
 ┃ ┃ ┃ ┃ ┗ 07
 ┃ ┃ ┃ ┃ ┃ ┗ sample_images_08.png
 ┃ ┣ blog
 ┃ ┃ ┗ 2023
 ┃ ┃ ┃ ┣ 10
 ┃ ┃ ┃ ┃ ┗ 27
 ┃ ┃ ┃ ┃ ┃ ┣ Untitled.png
 ┃ ┃ ┃ ┃ ┃ ┣ Untitled_1OJwIgn.png
 ┃ ┃ ┃ ┃ ┃ ┗ Untitled_uCdv6O8.png
 ┃ ┃ ┃ ┗ 11
 ┃ ┃ ┃ ┃ ┣ 06
 ┃ ┃ ┃ ┃ ┃ ┗ 서명.jpg
 ┃ ┃ ┃ ┃ ┗ 07
 ┃ ┃ ┃ ┃ ┃ ┣ sample_images_01.png
 ┃ ┃ ┃ ┃ ┃ ┣ sample_images_02.png
 ┃ ┃ ┃ ┃ ┃ ┣ sample_images_03.png
 ┃ ┃ ┃ ┃ ┃ ┣ sample_images_04.png
 ┃ ┃ ┃ ┃ ┃ ┣ sample_images_05.png
 ┃ ┃ ┃ ┃ ┃ ┣ sample_images_06.png
 ┃ ┃ ┃ ┃ ┃ ┗ sample_images_07.png
 ┃ ┗ file
 ┃ ┃ ┗ 2023
 ┃ ┃ ┃ ┗ 11
 ┃ ┃ ┃ ┃ ┗ 06
 ┣ migrations
 ┃ ┗ __init__.py
 ┣ static
 ┃ ┣ css
 ┃ ┃ ┣ chat.css
 ┃ ┃ ┣ common.css
 ┃ ┃ ┣ list.css
 ┃ ┃ ┣ login-join.css
 ┃ ┃ ┣ table.css
 ┃ ┃ ┣ view.css
 ┃ ┃ ┗ write.css
 ┃ ┣ django_tuieditor
 ┃ ┃ ┣ codemirror.css
 ┃ ┃ ┣ codemirror.js
 ┃ ┃ ┣ django-fixes.css
 ┃ ┃ ┣ django-fixes.js
 ┃ ┃ ┣ toastui-editor-viewer.css
 ┃ ┃ ┣ toastui-editor-viewer.js
 ┃ ┃ ┣ toastui-editor.css
 ┃ ┃ ┗ toastui-editor.js
 ┃ ┗ img
 ┃ ┃ ┣ default_user.jpg
 ┃ ┃ ┣ est.jpg
 ┃ ┃ ┣ first.png
 ┃ ┃ ┣ icon-search.png
 ┃ ┃ ┣ icon-x.png
 ┃ ┃ ┣ last.png
 ┃ ┃ ┣ licat.png
 ┃ ┃ ┣ next.png
 ┃ ┃ ┗ prev.png
 ┣ tutorialproject
 ┃ ┣ __init__.py
 ┃ ┣ asgi.py
 ┃ ┣ settings.py
 ┃ ┣ urls.py
 ┃ ┗ wsgi.py
 ┣ __init__.py
 ┣ admin.py
 ┣ apps.py
 ┣ db.sqlite3
 ┣ manage.py
 ┣ models.py
 ┣ nohup.out
 ┣ requirements.txt
 ┣ tests.py
 ┗ views.py
  • 4가지의 커스텀 앱과 2개의 외부 라이브러리를 사용합니다.
INSTALLED_APPS = [
    ...
    # 커스텀 앱
    'blog.apps.BlogConfig',
    'accounts.apps.AccountsConfig',
    'main.apps.MainConfig',
    'django_tuieditor.apps.DjangoTUIEditorConfig',
    # 외부 라이브러리
    'bootstrap5',
    'django_bootstrap_icons',
]
  • urls.py
urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('main.urls')),
    path('blog/', include('blog.urls')),
    path('accounts/', include('accounts.urls')),
    path('i18n/', include('django.conf.urls.i18n')),
]

데이터베이스 구조

erDiagram
    user ||--o{ post : write
    user {
      integer id PK
      varchar username
      varchar password
      image profile_image
      datetime created_at
      varchar ip_address
      datetime last_login
    }
    post }|--|{ tag : contains
    post ||--o| category : has
    post {
      integer id PK
      varchar title
      text content
      file file_upload
      image image_upload
      datetime created_at
      datetime updated_at
      varchar writer
      integer user_id FK
      integer hits
      integer tags FK
      varchar category FK
    }
    post ||--o{ comment : contains
    comment ||--o{ comment : contains
    comment {
      integer id PK
      integer parent FK
      text comment
      comment comment_reply FK
      datetime created_at
      datetime updated_at
    }
    
    tag {
      integer id PK
      varchar name
    }
    
    
    category {
      integer id PK
      varchar name
    }
Loading

기능명세서

  • CBV(Class Based View)로 작성되었습니다.

PageTitleViewMixin: 공통 상속 클래스

  • 페이지의 제목을 나타내는 클래스를 만들고 모든 View가 상속하도록 했습니다.
class PageTitleViewMixin:
  title = ""

  def get_title(self):
    return self.title
  def get_context_data(self, **kwargs):
    context = super().get_context_data(**kwargs)
    context['title'] = self.get_title()
    return context

BoardList(PageTitleViewMixin, ListView):

  • 작성된 글의 리스트화면 입니다.
  • 페이지네이션, 검색, 삭제(관리자) 기능이 있습니다.

BoardWrite(PageTitleViewMixin, PermissionRequiredMixin, CreateView):

  • 글 쓰기 화면 입니다.
  • 쓰기 권한을 검사합니다.
  • Toast-Ui-Editor 폼양식을 사용합니다.

BoardUpdate(PageTitleViewMixin, PermissionRequiredMixin, UpdateView):

  • 글 수정 화면입니다.
  • 수정 권한을 검사합니다.

BoardView(PageTitleViewMixin, DetailView):

  • 글 상세보기 화면 입니다.
  • Toast-Ui-Editor로 작성된 글을 마크업으로 보여줍니다
  • 선택한 글과 글에 달린 댓글과 대댓글을 보여줍니다.
  • get 요청 시 조회수가 1 증가합니다

BoardDelete(PageTitleViewMixin, PermissionRequiredMixin, DeleteView):

  • 글 삭제 View 입니다. 삭제권한을 확인합니다.

BoardDeleteMultiple(View):

  • 선택삭제 View 입니다. 관리자권한을 확인합니다.
  • post 요청 시 넘겨진 Post.id 값으로 삭제 합니다.

JoinUser(PageTitleViewMixin, CreateView):

  • 회원가입 화면 입니다.

loginUser(PageTitleViewMixin, LoginView):

  • 로그인 화면 입니다.

ProfileUser(PageTitleViewMixin, UpdateView):

  • 유저정보 화면 입니다. 로그인 권한을 검사합니다.
  • 정보 수정 기능이 있습니다.

PasswordChangeUser(PageTitleViewMixin, PasswordChangeView):

  • 비밀번호 변경 화면입니다. 로그인 권한을 검사합니다.

PasswordChangeDoneUser(PageTitleViewMixin, PasswordChangeDoneView):

  • 비밀번호 변경 완료 화면입니다. 로그인 권한을 검사합니다.

화면 설계

메인 로그인

main

로그인
회원가입 �정보수정
join profile
검색 번역
search translate
선택삭제 글쓰기
delete-muti write
글 상세보기 댓글
detail reply

관련 이슈

  • 댓글

댓글과 대댓글을 재귀로 호출하였습니다.

Comment 모델의 ForeinKey는 'self'로 자기자신을 참조하고 related_name으로 자신을 참조하는 댓글을 호출했습니다.

class Comment(models.Model):
  post = models.ForeignKey(Post, on_delete=models.CASCADE)
  comment_reply = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE, related_name='replies')
  comment_order = models.IntegerField(null=True, blank=True)
  user = models.ForeignKey(User, on_delete=models.CASCADE)
  content = models.TextField()
  created_at = models.DateTimeField(auto_now_add=True)
  updated_at = models.DateField(auto_now=True)

comment_form.html

<div class="media mb-4" id="comment_id_{{ comment.id }}">
  <div class="media-body">
    <h5>{{comment.user.get_image_tag}}<a class="btn btn-outline-success m-2" href="{{post.get_absolute_url}}?comment={{comment.id}}">Reply</a></h5>
    <h5 class="mt-0">{{comment.user}}</h5>
    <h5 class="mt-0">{{comment.created_at}}</h5>
    <h5 class="mt-0 bg-light text-body">{{comment.content}}</h5>

    {% for reply in comment.replies.all %}
      <div class="ms-4">
      {% include 'blog/recursive_comment.html' with comment=reply %}
      </div>
    {% endfor %}

  </div>
</div>

recursive_comment.html

<div class="media mb-4" id="comment_id_{{ comment.id }}">
  <div class="media-body">
    <h5>{{comment.user.get_image_tag}}<a class="btn btn-outline-success m-2" href="{{post.get_absolute_url}}?comment={{comment.id}}">Reply</a></h5>
    <h5 class="mt-0">{{comment.user}}</h5>
    <h5 class="mt-0">{{comment.created_at}}</h5>
    <h5 class="mt-0 bg-light text-body">{{comment.content}}</h5>

      {% for reply in comment.replies.all %}
        <div class="ms-4">
        {% include 'blog/recursive_comment.html' with comment=reply %}
        </div>
      {% endfor %}

  </div>
</div>
  • 회원가입 시 패스워드 해싱

회원가입 완료 후 로그인이 되지 않음

UserManager > create_user 에서 set_password 를 적용했습니다.

class CustomUserManager(UserManager):
    def _create_user(self, email, password, **extra_fields):
        if not email:
          raise ValueError('이메일은 필수입니다.')
        email = self.normalize_email(email)
        user = self.model(
          email = email,
          **extra_fields,
        )

        user.set_password(password) # Hash
        user.save(using=self._db)
        return user

회고

  • 잘한점/기억에 남는 점

블로그를 개발해 보면서 고려했던 것들 중 첫번째가 '속도' 였습니다.

장고는 기본적으로 상속하는 클래스가 많이 있어서 CRUD에 필요한 클래스들을 익혔던 것이 기억에 남습니다.

그리고 장고는 모델에 자기 자신을 참조하는 'self' 라는 독특한 문법을 가지고 있다는 것입니다.

  • 아쉬운 점

WISIWYG 에디터의 이미지를 따로 서버에 저장하는 것까지 구현하지 못한 것이 아쉽습니다.