Django 블로그 프로젝트 구현 - 완료된 배포 방식은 uwsgi 또는 gunicorn

프로젝트 흐름:

  1. 제품 요구사항

  • 사용자 인증 컴포넌트 및 Ajax를 활용한 로그인(이미지 캡차)
  • 폼 컴포넌트 및 Ajax를 사용한 사용자 가입 기능 1과 2의 데이터베이스 테이블은 사용자 정보 테이블에 통합
  • 시스템 홈 페이지 설계
  • 개인 웹사이트 설계 4의 개인 사이트 설계 필드는 title(개인 사이트 이름), site_name(https://xxx/alex 형식의 URL 후缀), theme(개인 사이트 스타일 CSS)이며, 사용자 정보 테이블과 일대일 연관
  • 개인 게시물 상세 페이지
  • 게시물 추천 기능
  • 댓글 기능 게시물 댓글 댓글에 대한 댓글
  • 관리자 페이지(부트스트랩 편집기)

2. 데이터베이스 구조 설계

2.1 사용자 정보 테이블 Django 기본 auth 기능을 사용하지만, 기본 User 테이블은 필드가 제한적이므로 커스터마이징 필요 AbstractUser 클래스를 상속하여 확장 가능 설정 파일에서 AUTH_USER_MODEL = "app02.CustomUser" 설정 필요

from django.contrib.auth.models import AbstractUser
class CustomUser(AbstractUser):
    phone_number = models.CharField(max_length=11, null=True, unique=True)
    avatar = models.FileField(upload_to="avatars/", default="avatars/default.png")
    create_time = models.DateTimeField(auto_now_add=True)
    blog = models.OneToOneField(to="Blog", to_field="nid", null=True, on_delete=models.CASCADE)

2.2 개인 홈페이지 테이블 홈페이지 제목, 사이트명, 스타일 정보 저장

class Blog(models.Model):
    title = models.CharField(max_length=64)
    sitename = models.CharField(max_length=64)
    theme = models.CharField(max_length=32)

2.3 카테고리 테이블 게시물 분류 기능, 1:1 관계로 사용자 정보 테이블 연결

class Category(models.Model):
    title = models.CharField(max_length=64)
    blog = models.ForeignKey(to="Blog", on_delete=models.CASCADE)

2.4 태그 테이블 게시물 키워드 관리, 사용자별 다중 태그 지원

class Tag(models.Model):
    title = models.CharField(max_length=64)
    blog = models.ForeignKey(to="Blog", on_delete=models.CASCADE)

2.5 게시물 테이블 게시물 본문, 좋아요, 비추천 수 등 메타 정보 관리

class Post(models.Model):
    title = models.CharField(max_length=64)
    content = models.TextField()
    up_count = models.IntegerField(default=0)
    down_count = models.IntegerField(default=0)
    user = models.ForeignKey(to="CustomUser", on_delete=models.CASCADE)
    category = models.ForeignKey(to="Category", on_delete=models.CASCADE)
    tags = models.ManyToManyField(
        to="Tag",
        through="PostTag",
        through_fields=("post", "tag")
    )

2.6 좋아요/비추천 관리 테이블

class Vote(models.Model):
    post = models.ForeignKey(to="Post", on_delete=models.CASCADE)
    user = models.ForeignKey(to="CustomUser", on_delete=models.CASCADE)
    is_up = models.BooleanField(default=True)
    class Meta:
        unique_together = [("post", "user")]

2.7 댓글 관리 테이블

class Comment(models.Model):
    post = models.ForeignKey(to="Post", on_delete=models.CASCADE)
    user = models.ForeignKey(to="CustomUser", on_delete=models.CASCADE)
    content = models.TextField()
    parent_comment = models.ForeignKey("self", null=True, on_delete=models.CASCADE)

3. 기능 개발 단계

로그인 기능

  • 폼 컴포넌트를 활용한 로그인/등록 화면 구성
  • PIL 모듈을 사용한 간단한 캡차 생성
  • 세션을 통한 캡차 검증 처리
  • Ajax를 이용한 실시간 유효성 검사
  • Django auth 모듈을 사용한 로그인 처리
  • 사용자 이미지 업로드 예览 기능 구현
  • FormData를 사용한 Ajax 요청 처리
$("#user_icon").change(function () {
    var file_obj = $(this)[0].files[0];
    var head_icon = new FileReader();
    head_icon.readAsDataURL(file_obj);
    head_icon.onload = function () {
        $(".head_pic").attr("src", head_icon.result)
    };
});

등록 폼 처리

  • 폼 필드 유효성 검사 및 오류 메시지 표시
  • 전역/로컬 훅을 사용한 사용자명 중복 검사
  • 암호 일치 여부 검증
  • 이미지 업로드 경로 설정 및 디렉토리 생성
  • MEDIA_ROOT 설정 및 URL 매핑
MEDIA_ROOT = os.path.join(BASE_DIR, "cnblogs/upload_folder/")
re_path("media/(?P<path>.*)/$", serve, {"document_root": settings.MEDIA_ROOT})

개인 홈페이지 구성

  • 사용자별 게시물 카테고리, 태그, 연도별 게시물 통계
  • ORM을 사용한 데이터 조회 및 정렬
  • 날짜 포맷팅을 위한 TruncDate 함수 활용
  • 동적 라우팅을 통해 카테고리/태그/아카이브 페이지 처리
def home_site(request, username, **kwargs):
    user_check = CustomUser.objects.filter(username=username).first()
    if not user_check:
        return render(request, "404.html")

    if kwargs:
        condition = kwargs.get("condition")
        parames = kwargs.get("parames")
        if condition == "category":
            posts = Post.objects.filter(user=user_check).filter(category__title=parames)
        elif condition == "tag":
            posts = Post.objects.filter(user=user_check).filter(tags__title=parames)
        elif condition == "archive":
            year, month = parames.split("-")
            posts = Post.objects.filter(user=user_check).filter(create_time__year=year, create_time__month=month)
    else:
        posts = user_check.post_set.all()

    categories = Category.objects.filter(blog=user_check.blog).annotate(c=Count("post")).values("title", "c")
    tags = Post.objects.filter(user=user_check).annotate(c=Count("tags")).values("title", "tag__title", "c")
    archives = Post.objects.filter(user=user_check).annotate(month=TruncMonth("create_time")).values("month").annotate(c=Count("nid"))

    return render(request, "home_site.html", locals())

댓글 기능

  • 부모/자식 댓글 구조 처리
  • Ajax를 통한 실시간 댓글 추가
  • 트리 구조로 댓글 목록 표시
  • 이메일 알림 및 보안 처리
@transaction.atomic
def comment(request):
    response_dict = {"status": None, "ret": None}
    if request.user.is_authenticated:
        if request.method == "POST" and request.POST.get("comment"):
            comment = request.POST.get("comment")
            user_id = request.user.pk
            article_id = request.POST.get("article_id")
            pid = request.POST.get("pid")
            with transaction.atomic():
                comment_obj = Comment.objects.create(article_id=article_id, user_id=user_id, content=comment, parent_comment_id=pid)
                Post.objects.filter(pk=article_id).update(comment_count=F("comment_count") + 1)
                t1 = Thread(target=send_mail, args=("알림", "새로운 댓글", settings.EMAIL_HOST_USER, ["test@example.com"]))
                t1.start()
            response_dict["create_time"] = comment_obj.creat_time.strftime("%Y-%m-%d %X")
            response_dict["user"] = request.user.username
    return JsonResponse(response_dict)

부트스트랩 편집기 구현

  • KindEditor 라이브러리 사용
  • 이미지 업로드 및 미리보기 기능
  • XSS 필터링 처리
KindEditor.ready(function (K) {
    window.editor = K.create('#editor_id', {
        uploadJson: "/upload_file/",
        extraFileUploadParams: {
            csrfmiddlewaretoken: $("[name='csrfmiddlewaretoken']").val(),
        },
        afterCreate: function () {
            this.sync();
        },
        afterBlur: function () {
            this.sync();
            content_html = this.html();
            content_text = this.text();
        }
    });
});

태그: Django AJAX ORM restframework XSS

6월 17일 16:03에 게시됨