- 요구사항
- 프로젝트 구조
- 데이터베이스 구조
- 기능명세서
- 화면설계
- 관련 이슈
- 회고
- 프로젝트 일정: 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)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
펼치기
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
}
- CBV(Class Based View)로 작성되었습니다.
- 페이지의 제목을 나타내는 클래스를 만들고 모든 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
- 작성된 글의 리스트화면 입니다.
- 페이지네이션, 검색, 삭제(관리자) 기능이 있습니다.
- 글 쓰기 화면 입니다.
- 쓰기 권한을 검사합니다.
- Toast-Ui-Editor 폼양식을 사용합니다.
- 글 수정 화면입니다.
- 수정 권한을 검사합니다.
- 글 상세보기 화면 입니다.
- Toast-Ui-Editor로 작성된 글을 마크업으로 보여줍니다
- 선택한 글과 글에 달린 댓글과 대댓글을 보여줍니다.
- get 요청 시 조회수가 1 증가합니다
- 글 삭제 View 입니다. 삭제권한을 확인합니다.
- 선택삭제 View 입니다. 관리자권한을 확인합니다.
- post 요청 시 넘겨진 Post.id 값으로 삭제 합니다.
- 회원가입 화면 입니다.
- 로그인 화면 입니다.
- 유저정보 화면 입니다. 로그인 권한을 검사합니다.
- 정보 수정 기능이 있습니다.
- 비밀번호 변경 화면입니다. 로그인 권한을 검사합니다.
- 비밀번호 변경 완료 화면입니다. 로그인 권한을 검사합니다.
메인 | 로그인 |
회원가입 | �정보수정 |
검색 | 번역 |
선택삭제 | 글쓰기 |
글 상세보기 | 댓글 |
- 댓글
댓글과 대댓글을 재귀로 호출하였습니다.
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 에디터의 이미지를 따로 서버에 저장하는 것까지 구현하지 못한 것이 아쉽습니다.