Post

CBV 사용한 DRF, MySQL 연동

클래스 기반 뷰(CBV)를 사용하여 Django Rest Framework(DRF)와 MySQL을 통합하는 방법을 다룹니다.

CBV 사용한 DRF, MySQL 연동

가상환경 생성 및 활성화

1
python -m venv venv
  • bash
1
source venv/Scripts/activate
  • powershell
1
.\venv\Scripts\activate

가상환경에 다운로드1

  • Django 다운로드

    1
    
     pip install Django==4.2
    
  • requirements.txt 파일에 내용 저장

    1
    
     pip freeze > requirements.txt
    

프로젝트 생성

1
django-admin startproject project03 .

가상환경에 다운로드2

  • python manage.py shell 명령어로 쉘을 열 수 있지만 Django 기본 Shell보다 더 많은 기능이 있는 shell_plus를 제공하고 있습니다.
  • django-extensions 다운로드

    1
    
     pip install django-extensions
    
  • django-extensions 설정
    • settings.py 파일에 INSTALLED_APPS 에 추가하기
    1
    
     "django_extensions",
    
    • 실행은 python manage.py shell_plus 이 명령어로 합니다.
  • ipython은 python 기본 Shell에 여러가지 기능을 더한것입니다.
  • 예를 들어 자동완성, 코드 색상 강조와 같은 기능이 있습니다.
  • ipython 다운로드
1
pip install ipython

앱 생성

1
python manage.py startapp accounts
1
python manage.py startapp posts
1
python manage.py startapp core

앱 등록

1
2
3
4
5
6
7
8
9
10
11
12
13
   INSTALLED_APPS = [
      'django.contrib.admin',
      'django.contrib.auth',
      'django.contrib.contenttypes',
      'django.contrib.sessions',
      'django.contrib.messages',
      'django.contrib.staticfiles',

      # Local app
      'accounts',
      'posts',
      'core',
   ]

앱 URL 분리

1
2
3
4
5
6
7
8
9
10
   from django.contrib import admin
   from django.urls import path, include

   urlpatterns = [
      path('admin/', admin.site.urls),
      path('accounts/', include('accounts.urls')),
      path('posts/', include('posts.urls')),
      path('', include('core.urls')),
   ]

  • 각 앱에 urls.py 파일 생성

    1
    2
    3
    4
    5
    
     from django.urls import path
     from . import views
    
     urlpatterns = []
    
    
    1
    2
    3
    4
    5
    6
    
     from django.urls import path
     from . import views
    
     app_name = "accounts"
    
     urlpatterns = []
    
    1
    2
    3
    4
    5
    6
    
     from django.urls import path
     from . import views
    
     app_name = "posts"
    
     urlpatterns = []
    

base.html 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
   TEMPLATES = [
      {
         'BACKEND': 'django.template.backends.django.DjangoTemplates',
         'DIRS': [BASE_DIR /'templates'],
         'APP_DIRS': True,
         'OPTIONS': {
               'context_processors': [
                  'django.template.context_processors.debug',
                  'django.template.context_processors.request',
                  'django.contrib.auth.context_processors.auth',
                  'django.contrib.messages.context_processors.messages',
               ],
         },
      },
   ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
   
   <!DOCTYPE html>
   <html lang="en">
   <head>
      <meta charset="UTF-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <title>{% block title %}Default Title{% endblock %}</title>
   </head>
   <body>
      <header>{% block header %}Default Header{% endblock %}</header>
      <main>{% block content %}Default Content{% endblock %}</main>
      <footer>{% block footer %}Default Footer{% endblock %}</footer>
   </body>
   </html>
   

core 앱 설정

1
2
3
4
5
6
7
from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index'),  # 메인 페이지 URL 패턴
]

1
2
3
4
5
from django.shortcuts import render

def index(request):
    return render(request, "core/index.html")

1
2
3
4
5
6
7
{% extends "base.html" %}

{% block content %}
<h1>index</h1>
{% endblock %}

  • base.html 수정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>{% block title %}Django Project{% endblock %}</title>
    </head>
    <body>
        <header>
            {% block header %}
            <nav>
                <a href="{% url 'index' %}"></a>
            </nav>
            {% endblock %}
        </header>
        <main>{% block content %}메인 글{% endblock %}</main>
        <footer>{% block footer %}Footer{% endblock %}</footer>
    </body>
</html>

추가 TIP

  • 언어 설정
1
2
3
4
5
6
7
LANGUAGE_CODE = 'ko-kr'

TIME_ZONE = 'Asia/Seoul'

USE_I18N = True

USE_TZ = True

Custom User Model 정의

1
2
3
4
5
6
7
8
9
# 로그인 페이지로 이동할 URL
LOGIN_URL = 'accounts:login'
# 로그인 성공 후 이동할 URL
LOGIN_REDIRECT_URL = '/'
# 로그아웃 후 이동할 URL
LOGOUT_REDIRECT_URL = '/'

# Custom Model
AUTH_USER_MODEL='accounts.user'
1
2
3
4
5
from django.db import models
from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    bio = models.CharField(max_length=255, default='Default Bio')
  • 마이그레이션 생성

    1
    
     python manage.py makemigrations
    
  • 마이그레이션 저장

    1
    
     python manage.py migrate
    
  • 마이그레이션 저장 확인

    1
    
     python manage.py showmigrations
    

추상화 사용한 posts 앱의 모델 정의

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from django.conf import settings
from django.db import models

class TimestampedModel(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True

class Post(TimestampedModel):
    title = models.CharField(max_length=50)
    content = models.TextField()
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True
    )
    likes = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='liked_posts', blank=True)

    def __str__(self):
        return self.title

    def total_likes(self):
        return self.likes.count()

class Comment(TimestampedModel):
    post = models.ForeignKey('Post', related_name='comments', on_delete=models.CASCADE)
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True
    )
    content = models.TextField()

    def __str__(self):
        return f"{self.author.username if self.author else '알 수 없는 사용자'} - {self.content[:20]}"

  • 마이그레이션 다시 진행

    1
    2
    3
    
     python manage.py makemigrations posts
     python manage.py migrate posts
     python manage.py showmigrations
    

admin 등록해서 관리자 페이지에서 관리 가능하도록 설정

1
2
3
4
5
6
from django.contrib import admin
from .models import User

@admin.register(User)
class UserAdmin(admin.ModelAdmin):
    pass
1
2
3
4
5
6
7
8
9
10
from django.contrib import admin
from .models import Post, Comment

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    pass

@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
    pass

createsuperuser 생성

1
python manage.py createsuperuser

회원 관련 기능 추가

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import UserCreationForm, PasswordChangeForm

# CustomUserForm: 회원가입 폼
class CustomUserForm(UserCreationForm):
    class Meta:
        model = get_user_model()
        fields = ["username", "email", "bio", "password1", "password2"]

# PasswordChangeForm: 비밀번호 변경 폼
class CustomPasswordChangeForm(PasswordChangeForm):
    class Meta:
        model = get_user_model()
        fields = ["old_password", "new_password1", "new_password2"]

# ProfileUpdateForm: 프로필 수정 폼
class ProfileUpdateForm(forms.ModelForm):
    class Meta:
        model = get_user_model()
        fields = ["username", "email", "bio"]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
from django.urls import reverse_lazy  # URL 패턴을 문자열로 반환하는 유틸리티
from django.views.generic.edit import CreateView, UpdateView  # 제네릭 뷰: 생성(Create), 업데이트(Update)
from django.contrib.auth.views import LoginView, LogoutView, PasswordChangeView  # 로그인, 로그아웃, 비밀번호 변경 뷰
from django.views.generic.detail import DetailView  # 제네릭 뷰: 상세 보기
from django.contrib.auth.mixins import LoginRequiredMixin  # 로그인을 요구하는 믹스인
from .forms import CustomUserForm, CustomPasswordChangeForm, ProfileUpdateForm  # 커스텀 폼 클래스들
from django.contrib.auth import get_user_model  # 현재 프로젝트에서 사용 중인 User 모델을 가져옴

# 모듈 수준 변수
# 현재 프로젝트에서 사용 중인 User 모델
User = get_user_model()

# 회원가입 뷰
class SignupView(CreateView):
    template_name = "accounts/signup.html"  # 사용할 템플릿 파일 경로
    form_class = CustomUserForm  # 회원가입 폼 클래스
    success_url = reverse_lazy('accounts:login')  # 회원가입 성공 시 로그인 페이지로 리다이렉트

# 로그인 뷰
class UserLoginView(LoginView):
    template_name = "accounts/login.html"  # 사용할 템플릿 파일 경로

# 로그아웃 뷰
class UserLogoutView(LogoutView):
    # LogoutView는 기본적으로 별도의 설정 없이 작동하므로 추가 코드 필요 없음
    pass

# 프로필 보기 뷰
class ProfileView(LoginRequiredMixin, DetailView):
    model = User  # User 모델을 사용
    template_name = "accounts/profile.html"  # 사용할 템플릿 파일 경로

    def get_object(self, queryset=None):
        # 현재 로그인한 사용자의 프로필 정보를 반환
        return self.request.user

# 프로필 수정 뷰
class ProfileUpdateView(LoginRequiredMixin, UpdateView):
    model = User  # User 모델을 사용
    form_class = ProfileUpdateForm  # 프로필 수정 폼 클래스
    template_name = "accounts/profile_update.html"  # 사용할 템플릿 파일 경로
    success_url = reverse_lazy('accounts:profile')  # 수정 성공 시 프로필 페이지로 리다이렉트

    def get_object(self, queryset=None):
        # 현재 로그인한 사용자의 정보를 수정
        return self.request.user

# 비밀번호 변경 뷰
class ChangePasswordView(LoginRequiredMixin, PasswordChangeView):
    form_class = CustomPasswordChangeForm  # 비밀번호 변경 폼 클래스
    template_name = "accounts/change_password.html"  # 사용할 템플릿 파일 경로
    success_url = reverse_lazy('accounts:profile')  # 변경 성공 시 프로필 페이지로 리다이렉트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from django.urls import path
from .views import SignupView, UserLoginView, UserLogoutView, ProfileView, ProfileUpdateView, ChangePasswordView

app_name = "accounts"

urlpatterns = [
    path("signup/", SignupView.as_view(), name="signup"),
    path("login/", UserLoginView.as_view(), name="login"),
    path("logout/", UserLogoutView.as_view(), name="logout"),
    path("profile/", ProfileView.as_view(), name="profile"),
    path("profile-update/", ProfileUpdateView.as_view(), name="profile_update"),
    path("change-password/", ChangePasswordView.as_view(), name="change_password"),
]

views.py에서 연결할 템플릿 추가하기

게시글 CRUD 관련 기능 추가

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from django import forms
from .models import Post, Comment

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ['title', 'content']  # 사용자 입력 필드만 포함
        widgets = {
            'title': forms.TextInput(attrs={'placeholder': '제목을 입력하세요', 'class': 'form-control'}),
            'content': forms.Textarea(attrs={'placeholder': '내용을 입력하세요', 'class': 'form-control'}),
        }

class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ['content']  # 댓글 내용만 입력받음
        widgets = {
            'content': forms.Textarea(attrs={'placeholder': '댓글을 입력하세요', 'class': 'form-control', 'rows': 3}),
        }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
from django.shortcuts import render, get_object_or_404, redirect  # HTTP 응답, 객체 조회 및 리다이렉트를 위한 유틸리티 함수
from django.urls import reverse_lazy  # URL 패턴을 문자열로 반환하는 유틸리티
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView, View  # 제네릭 뷰 클래스
from django.http import JsonResponse  # JSON 응답 생성을 위한 유틸리티
from django.contrib.auth.mixins import LoginRequiredMixin  # 로그인 필요 여부를 제어하는 믹스인
from .models import Post, Comment  # Post와 Comment 모델
from .forms import PostForm, CommentForm  # Post와 Comment에 대한 폼 클래스

# 게시글 목록을 보여주는 뷰
class PostListView(ListView):
    model = Post  # 어떤 모델을 다룰지 지정
    template_name = "posts/post_list.html"  # 사용할 템플릿 파일 경로
    context_object_name = "posts"  # 템플릿에서 사용할 컨텍스트 이름
    ordering = ['-created_at']  # 게시글 정렬 순서 (최신순)

# 게시글 상세 보기 뷰
class PostDetailView(DetailView):
    model = Post
    template_name = "posts/post_detail.html"
    context_object_name = "post"

    def get_context_data(self, **kwargs):
        # 추가 컨텍스트 데이터를 템플릿에 전달
        context = super().get_context_data(**kwargs)
        context['comment_form'] = CommentForm()  # 댓글 작성 폼
        return context

    def post(self, request, *args, **kwargs):
        # 댓글 작성 요청을 처리
        self.object = self.get_object()  # 현재 게시글 객체
        form = CommentForm(request.POST)  # POST 요청에서 데이터 가져오기
        if form.is_valid():
            comment = form.save(commit=False)  # 데이터베이스에 저장하지 않고 객체 생성
            comment.post = self.object  # 댓글이 달린 게시글 설정
            comment.author = request.user  # 현재 유저를 댓글 작성자로 설정
            comment.save()  # 데이터베이스에 저장
            return redirect('posts:post_detail', pk=self.object.pk)  # 상세 페이지로 리다이렉트
        return self.get(request, *args, **kwargs)

# 게시글 작성 뷰
class PostCreateView(LoginRequiredMixin, CreateView):
    model = Post
    form_class = PostForm  # Post 모델에 대한 폼
    template_name = "posts/post_form.html"
    success_url = reverse_lazy('posts:post_list')  # 성공 시 리다이렉트할 URL

    def form_valid(self, form):
        # 폼 검증이 성공하면 호출됨
        form.instance.author = self.request.user  # 현재 유저를 게시글 작성자로 설정
        return super().form_valid(form)

# 게시글 수정 뷰
class PostUpdateView(LoginRequiredMixin, UpdateView):
    model = Post
    form_class = PostForm
    template_name = "posts/post_form.html"
    success_url = reverse_lazy('posts:post_list')

    def get_queryset(self):
        # 작성자 본인의 게시글만 수정 가능
        return Post.objects.filter(author=self.request.user)

# 게시글 삭제 뷰
class PostDeleteView(LoginRequiredMixin, DeleteView):
    model = Post
    success_url = reverse_lazy('posts:post_list')

    def get_queryset(self):
        # 작성자 본인의 게시글만 삭제 가능
        return Post.objects.filter(author=self.request.user)

# 게시글 좋아요 토글 뷰
class PostLikeToggleView(View):
    def post(self, request, pk):
        post = get_object_or_404(Post, pk=pk)  # 게시글 객체 가져오기
        
        if not request.user.is_authenticated:
            # 로그인되지 않은 사용자는 로그인 페이지로 리다이렉트
            login_url = f"{reverse_lazy('accounts:login')}?next={reverse_lazy('posts:post_detail', kwargs={'pk': pk})}"
            return redirect(login_url)

        if request.user in post.likes.all():
            # 이미 좋아요를 누른 경우 취소
            post.likes.remove(request.user)
        else:
            # 좋아요 추가
            post.likes.add(request.user)
        
        return redirect('posts:post_detail', pk=pk)

# 댓글 수정 뷰
class CommentUpdateView(LoginRequiredMixin, UpdateView):
    model = Comment
    form_class = CommentForm
    template_name = "posts/post_detail.html"

    def get_context_data(self, **kwargs):
        # 댓글과 연관된 게시글 정보 추가
        context = super().get_context_data(**kwargs)
        context['post'] = self.object.post
        return context

    def form_valid(self, form):
        # 폼 검증이 성공하면 댓글 저장 후 게시글 상세 페이지로 리다이렉트
        self.object = form.save()
        return redirect('posts:post_detail', pk=self.object.post.pk)

    def get_queryset(self):
        # 본인의 댓글만 수정 가능
        return Comment.objects.filter(author=self.request.user)

# 댓글 삭제 뷰
class CommentDeleteView(LoginRequiredMixin, DeleteView):
    model = Comment

    def get_queryset(self):
        # 본인의 댓글만 삭제 가능
        return Comment.objects.filter(author=self.request.user)

    def get_success_url(self):
        # 삭제 후 댓글이 달린 게시글로 리다이렉트
        post = self.object.post
        return reverse_lazy('posts:post_detail', kwargs={'pk': post.pk})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from django.urls import path
from .views import PostListView, PostDetailView, PostCreateView, PostUpdateView, PostDeleteView, PostLikeToggleView, CommentUpdateView, CommentDeleteView

app_name = 'posts'

urlpatterns = [
    path('post-list/', PostListView.as_view(), name='post_list'),
    path('post-detail/<int:pk>/', PostDetailView.as_view(), name='post_detail'),
    path('post-create/', PostCreateView.as_view(), name='post_create'),
    path('post-update/<int:pk>/', PostUpdateView.as_view(), name='post_update'),
    path('post-delete/<int:pk>/', PostDeleteView.as_view(), name='post_delete'),
    path('post-like/<int:pk>/', PostLikeToggleView.as_view(), name='post_like_toggle'),
    path('comment-update/<int:pk>/', CommentUpdateView.as_view(), name='comment_update'),
    path('comment-delete/<int:pk>/', CommentDeleteView.as_view(), name='comment_delete'),
]

views.py에서 연결할 템플릿 추가하기

최종 base.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>{% block title %}Django Project{% endblock %}</title>
    </head>
    <body>
        <header>
            {% block header %}
            <nav>
                <a href="{% url 'index' %}"></a>
                <a href="{% url 'posts:post_list' %}">게시글</a>
                
                {% if user.is_authenticated %}
                    <!-- 로그인한 사용자에게만 표시 -->
                    <form method="post" action="{% url 'accounts:logout' %}" style="display: inline;">
                        {% csrf_token %}
                        <button type="submit">Logout</button>
                    </form>
                    <a href="{% url 'accounts:profile' %}">프로필</a>
                {% else %}
                    <!-- 비로그인 사용자에게만 표시 -->
                    <a href="{% url 'accounts:signup' %}">회원가입</a>
                    <a href="{% url 'accounts:login' %}">로그인</a>
                {% endif %}
            </nav>
            {% endblock %}
        </header>
        <main>{% block content %}메인 글{% endblock %}</main>
        <footer>{% block footer %}Footer{% endblock %}</footer>
    </body>
</html>

DRF로 변환하기

  • DRF 설치 및 설정
1
pip install djangorestframework
  • 'rest_framework', 추가
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
INSTALLED_APPS = [
   'django.contrib.admin',
   'django.contrib.auth',
   'django.contrib.contenttypes',
   'django.contrib.sessions',
   'django.contrib.messages',
   'django.contrib.staticfiles',

   # Third party
   "django_extensions",
   "django_seed",
   'rest_framework',

   # Local app
   'accounts',
   'posts',
   'core',

]
  • requirements.txt 파일에 내용 저장
1
pip freeze > requirements.txt
  • JWT 설치 및 설정
1
pip install djangorestframework-simplejwt
  • REST_FRAMEWORK 추가
1
2
3
4
5
REST_FRAMEWORK = {
   "DEFAULT_AUTHENTICATION_CLASSES": [
      "rest_framework_simplejwt.authentication.JWTAuthentication",
   ],
}
  • requirements.txt 파일에 내용 저장
1
pip freeze > requirements.txt

posts 앱에 API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from rest_framework import serializers
from .models import Post, Comment

class CommentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Comment
        fields = ['id', 'content', 'created_at', 'author', 'post']
        read_only_fields = ['author', 'post', 'created_at']

class PostSerializer(serializers.ModelSerializer):
    total_likes = serializers.SerializerMethodField()

    class Meta:
        model = Post
        fields = ['id', 'title', 'content', 'author', 'created_at', 'updated_at', 'total_likes']
        read_only_fields = ['author', 'created_at', 'updated_at', 'total_likes']

    def get_total_likes(self, obj):
        return obj.total_likes()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
from django.shortcuts import render, get_object_or_404, redirect  # HTTP 응답, 객체 조회 및 리다이렉트를 위한 유틸리티 함수
from django.urls import reverse_lazy  # URL 패턴을 문자열로 반환하는 유틸리티
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView, View  # 제네릭 뷰 클래스
from django.contrib.auth.mixins import LoginRequiredMixin  # 로그인 필요 여부를 제어하는 믹스인
from .models import Post, Comment  # Post와 Comment 모델
from .forms import PostForm, CommentForm  # Post와 Comment에 대한 폼 클래스

from rest_framework.views import APIView
from rest_framework.response import Response
from .serializers import PostSerializer, CommentSerializer
from rest_framework.generics import (
    RetrieveAPIView, CreateAPIView, UpdateAPIView, DestroyAPIView, ListAPIView,
)
from rest_framework.permissions import IsAuthenticated

# 게시글 목록 API
class PostListAPIView(APIView):
    def get(self, request, *args, **kwargs):
        posts = Post.objects.all().order_by('-created_at')  # 최신순 정렬
        serializer = PostSerializer(posts, many=True)
        return Response(serializer.data)

# 게시글 상세 보기 API
class PostDetailAPIView(RetrieveAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

# 게시글 작성 API
class PostCreateAPIView(CreateAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = [IsAuthenticated]  # 로그인한 사용자만 생성 가능

    def perform_create(self, serializer):
        # 현재 요청한 사용자를 작성자로 설정
        serializer.save(author=self.request.user)

# 게시글 수정 API
class PostUpdateAPIView(UpdateAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = [IsAuthenticated]  # 로그인한 사용자만 수정 가능

    def get_queryset(self):
        # 현재 사용자가 작성한 게시글만 반환
        return Post.objects.filter(author=self.request.user)

# 게시글 삭제 API
class PostDeleteAPIView(DestroyAPIView):
    queryset = Post.objects.all()
    permission_classes = [IsAuthenticated]  # 로그인한 사용자만 삭제 가능

    def get_queryset(self):
        # 현재 사용자가 작성한 게시글만 삭제 가능
        return Post.objects.filter(author=self.request.user)

# 댓글 조회 API
class CommentListAPIView(ListAPIView):
    serializer_class = CommentSerializer

    def get_queryset(self):
        # 특정 게시글(post_id)에 달린 댓글만 반환
        post_id = self.kwargs.get('post_id')  # URL에서 post_id를 가져옴
        return Comment.objects.filter(post__id=post_id).order_by('created_at')

# 댓글 생성 API
class CommentCreateAPIView(CreateAPIView):
    serializer_class = CommentSerializer
    permission_classes = [IsAuthenticated]  # 로그인한 사용자만 생성 가능

    def perform_create(self, serializer):
        # 댓글을 특정 게시글에 연결
        post_id = self.kwargs.get('post_id')  # URL에서 post_id를 가져옴
        post = Post.objects.get(id=post_id)
        serializer.save(post=post, author=self.request.user)

# 댓글 수정 API
class CommentUpdateAPIView(UpdateAPIView):
    serializer_class = CommentSerializer
    permission_classes = [IsAuthenticated]  # 로그인한 사용자만 수정 가능

    def get_queryset(self):
        # 현재 사용자가 작성한 댓글만 수정 가능
        return Comment.objects.filter(author=self.request.user)

# 댓글 삭제 API
class CommentDeleteAPIView(DestroyAPIView):
    queryset = Comment.objects.all()
    permission_classes = [IsAuthenticated]  # 로그인한 사용자만 삭제 가능

    def get_queryset(self):
        # 현재 사용자가 작성한 댓글만 삭제 가능
        return Comment.objects.filter(author=self.request.user)

# 좋아요를 추가하거나 제거할 수 있는 API
class PostLikeToggleAPIView(APIView):
    permission_classes = [IsAuthenticated]  # 로그인한 사용자만 접근 가능

    def post(self, request, pk, *args, **kwargs):
        post = get_object_or_404(Post, pk=pk)  # 게시글 가져오기
        user = request.user

        if user in post.likes.all():
            # 좋아요 취소
            post.likes.remove(user)
            liked = False
        else:
            # 좋아요 추가
            post.likes.add(user)
            liked = True

        return Response({
            "liked": liked,
            "total_likes": post.total_likes()
        })

# 게시글 목록을 보여주는 뷰
class PostListView(ListView):
    model = Post  # 어떤 모델을 다룰지 지정
    template_name = "posts/post_list.html"  # 사용할 템플릿 파일 경로
    context_object_name = "posts"  # 템플릿에서 사용할 컨텍스트 이름
    ordering = ['-created_at']  # 게시글 정렬 순서 (최신순)

# 게시글 상세 보기 뷰
class PostDetailView(DetailView):
    model = Post
    template_name = "posts/post_detail.html"
    context_object_name = "post"

    def get_context_data(self, **kwargs):
        # 추가 컨텍스트 데이터를 템플릿에 전달
        context = super().get_context_data(**kwargs)
        context['comment_form'] = CommentForm()  # 댓글 작성 폼
        return context

    def post(self, request, *args, **kwargs):
        # 댓글 작성 요청을 처리
        self.object = self.get_object()  # 현재 게시글 객체
        form = CommentForm(request.POST)  # POST 요청에서 데이터 가져오기
        if form.is_valid():
            comment = form.save(commit=False)  # 데이터베이스에 저장하지 않고 객체 생성
            comment.post = self.object  # 댓글이 달린 게시글 설정
            comment.author = request.user  # 현재 유저를 댓글 작성자로 설정
            comment.save()  # 데이터베이스에 저장
            return redirect('posts:post_detail', pk=self.object.pk)  # 상세 페이지로 리다이렉트
        return self.get(request, *args, **kwargs)

# 게시글 작성 뷰
class PostCreateView(LoginRequiredMixin, CreateView):
    model = Post
    form_class = PostForm  # Post 모델에 대한 폼
    template_name = "posts/post_form.html"
    success_url = reverse_lazy('posts:post_list')  # 성공 시 리다이렉트할 URL

    def form_valid(self, form):
        # 폼 검증이 성공하면 호출됨
        form.instance.author = self.request.user  # 현재 유저를 게시글 작성자로 설정
        return super().form_valid(form)

# 게시글 수정 뷰
class PostUpdateView(LoginRequiredMixin, UpdateView):
    model = Post
    form_class = PostForm
    template_name = "posts/post_form.html"
    success_url = reverse_lazy('posts:post_list')

    def get_queryset(self):
        # 작성자 본인의 게시글만 수정 가능
        return Post.objects.filter(author=self.request.user)

# 게시글 삭제 뷰
class PostDeleteView(LoginRequiredMixin, DeleteView):
    model = Post
    success_url = reverse_lazy('posts:post_list')

    def get_queryset(self):
        # 작성자 본인의 게시글만 삭제 가능
        return Post.objects.filter(author=self.request.user)

# 게시글 좋아요 토글 뷰
class PostLikeToggleView(View):
    def post(self, request, pk):
        post = get_object_or_404(Post, pk=pk)  # 게시글 객체 가져오기
        
        if not request.user.is_authenticated:
            # 로그인되지 않은 사용자는 로그인 페이지로 리다이렉트
            login_url = f"{reverse_lazy('accounts:login')}?next={reverse_lazy('posts:post_detail', kwargs={'pk': pk})}"
            return redirect(login_url)

        if request.user in post.likes.all():
            # 이미 좋아요를 누른 경우 취소
            post.likes.remove(request.user)
        else:
            # 좋아요 추가
            post.likes.add(request.user)
        
        return redirect('posts:post_detail', pk=pk)

# 댓글 수정 뷰
class CommentUpdateView(LoginRequiredMixin, UpdateView):
    model = Comment
    form_class = CommentForm
    template_name = "posts/post_detail.html"

    def get_context_data(self, **kwargs):
        # 댓글과 연관된 게시글 정보 추가
        context = super().get_context_data(**kwargs)
        context['post'] = self.object.post
        return context

    def form_valid(self, form):
        # 폼 검증이 성공하면 댓글 저장 후 게시글 상세 페이지로 리다이렉트
        self.object = form.save()
        return redirect('posts:post_detail', pk=self.object.post.pk)

    def get_queryset(self):
        # 본인의 댓글만 수정 가능
        return Comment.objects.filter(author=self.request.user)

# 댓글 삭제 뷰
class CommentDeleteView(LoginRequiredMixin, DeleteView):
    model = Comment

    def get_queryset(self):
        # 본인의 댓글만 삭제 가능
        return Comment.objects.filter(author=self.request.user)

    def get_success_url(self):
        # 삭제 후 댓글이 달린 게시글로 리다이렉트
        post = self.object.post
        return reverse_lazy('posts:post_detail', kwargs={'pk': post.pk})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from django.urls import path
from .views import (
    PostListAPIView, 
    PostDetailAPIView,
    PostCreateAPIView,
    PostUpdateAPIView,
    PostDeleteAPIView,
    CommentListAPIView,
    CommentCreateAPIView,
    CommentUpdateAPIView,
    CommentDeleteAPIView,
    PostLikeToggleAPIView,

    PostListView, 
    PostDetailView, 
    PostCreateView, 
    PostUpdateView, 
    PostDeleteView, 
    PostLikeToggleView, 
    CommentUpdateView, 
    CommentDeleteView,
)

app_name = 'posts'

urlpatterns = [
    path('post-list/', PostListView.as_view(), name='post_list'),
    path('post-detail/<int:pk>/', PostDetailView.as_view(), name='post_detail'),
    path('post-create/', PostCreateView.as_view(), name='post_create'),
    path('post-update/<int:pk>/', PostUpdateView.as_view(), name='post_update'),
    path('post-delete/<int:pk>/', PostDeleteView.as_view(), name='post_delete'),
    path('post-like/<int:pk>/', PostLikeToggleView.as_view(), name='post_like_toggle'),
    path('comment-update/<int:pk>/', CommentUpdateView.as_view(), name='comment_update'),
    path('comment-delete/<int:pk>/', CommentDeleteView.as_view(), name='comment_delete'),
]

urlpatterns += [
    path('api/', PostListAPIView.as_view(), name='api_post_list'),
    path('api/<int:pk>/', PostDetailAPIView.as_view(), name='api_post_detail'),
    path('api/create/', PostCreateAPIView.as_view(), name='api_post_create'),
    path('api/update/<int:pk>/', PostUpdateAPIView.as_view(), name='api_post_update'),
    path('api/delete/<int:pk>/', PostDeleteAPIView.as_view(), name='api_post_delete'),
    path('api/<int:post_id>/comments/', CommentListAPIView.as_view(), name='api_comment_list'),
    path('api/<int:post_id>/comments/create/', CommentCreateAPIView.as_view(), name='api_comment_create'),
    path('api/comments/update/<int:pk>/', CommentUpdateAPIView.as_view(), name='api_comment_update'),
    path('api/comments/delete/<int:pk>/', CommentDeleteAPIView.as_view(), name='api_comment_delete'),
    path('api/<int:pk>/like/', PostLikeToggleAPIView.as_view(), name='api_post_like_toggle'),
]

accounts 앱에 API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from rest_framework import serializers
from .models import User

class ChangePasswordSerializer(serializers.Serializer):
    old_password = serializers.CharField(required=True)
    new_password = serializers.CharField(required=True)

    def validate_new_password(self, value):
        if len(value) < 8:
            raise serializers.ValidationError("비밀번호는 최소 8자 이상이어야 합니다.")
        return value

class UserProfileSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['id', 'username', 'email', 'bio', 'date_joined']
        read_only_fields = ['id', 'date_joined']

class SignupSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['username', 'password', 'email', 'bio']  # 회원가입 필드
        extra_kwargs = {
            'password': {'write_only': True}  # 비밀번호는 쓰기 전용
        }

    def create(self, validated_data):
        user = User.objects.create_user(
            username=validated_data['username'],
            password=validated_data['password'],
            email=validated_data.get('email', ''),
            bio=validated_data.get('bio', 'Default Bio')
        )
        return user

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
from django.urls import reverse_lazy  # URL 패턴을 문자열로 반환하는 유틸리티
from django.views.generic.edit import CreateView, UpdateView  # 제네릭 뷰: 생성(Create), 업데이트(Update)
from django.contrib.auth.views import LoginView, LogoutView, PasswordChangeView  # 로그인, 로그아웃, 비밀번호 변경 뷰
from django.views.generic.detail import DetailView  # 제네릭 뷰: 상세 보기
from django.contrib.auth.mixins import LoginRequiredMixin  # 로그인을 요구하는 믹스인
from .forms import CustomUserForm, CustomPasswordChangeForm, ProfileUpdateForm  # 커스텀 폼 클래스들
from django.contrib.auth import get_user_model  # 현재 프로젝트에서 사용 중인 User 모델을 가져옴

from rest_framework.generics import (
    CreateAPIView, RetrieveAPIView, UpdateAPIView
    )
from .serializers import SignupSerializer, UserProfileSerializer, ChangePasswordSerializer
from rest_framework.permissions import IsAuthenticated
from .models import User
from rest_framework.response import Response
from django.contrib.auth.hashers import check_password

# 모듈 수준 변수
# 현재 프로젝트에서 사용 중인 User 모델
User = get_user_model()

# 회원가입 API 뷰
class SignupAPIView(CreateAPIView):
    serializer_class = SignupSerializer
    queryset = User.objects.all()  # 전체 사용자 조회

# 회원 프로필 API 뷰
class ProfileAPIView(RetrieveAPIView):
    queryset = User.objects.all()
    serializer_class = UserProfileSerializer
    permission_classes = [IsAuthenticated]  # 로그인한 사용자만 접근 가능

    def get_object(self):
        # 현재 로그인한 사용자 반환
        return self.request.user

# 프로필 수정 API 뷰
class ProfileUpdateAPIView(UpdateAPIView):
    queryset = User.objects.all()
    serializer_class = UserProfileSerializer
    permission_classes = [IsAuthenticated]  # 로그인한 사용자만 수정 가능

    def get_object(self):
        # 현재 로그인한 사용자 반환
        return self.request.user

# 비밀번호 변경 API 뷰
class ChangePasswordAPIView(UpdateAPIView):
    serializer_class = ChangePasswordSerializer
    permission_classes = [IsAuthenticated]  # 로그인한 사용자만 접근 가능

    def update(self, request, *args, **kwargs):
        user = self.request.user
        serializer = self.get_serializer(data=request.data)

        if serializer.is_valid():
            old_password = serializer.validated_data['old_password']
            new_password = serializer.validated_data['new_password']

            # 기존 비밀번호 확인
            if not check_password(old_password, user.password):
                return Response({"error": "현재 비밀번호가 올바르지 않습니다."}, status=400)

            # 새 비밀번호 설정
            user.set_password(new_password)
            user.save()

            return Response({"message": "비밀번호가 성공적으로 변경되었습니다."}, status=200)

        return Response(serializer.errors, status=400)

# 회원가입 뷰
class SignupView(CreateView):
    template_name = "accounts/signup.html"  # 사용할 템플릿 파일 경로
    form_class = CustomUserForm  # 회원가입 폼 클래스
    success_url = reverse_lazy('accounts:login')  # 회원가입 성공 시 로그인 페이지로 리다이렉트

# 로그인 뷰
class UserLoginView(LoginView):
    template_name = "accounts/login.html"  # 사용할 템플릿 파일 경로

# 로그아웃 뷰
class UserLogoutView(LogoutView):
    # LogoutView는 기본적으로 별도의 설정 없이 작동하므로 추가 코드 필요 없음
    pass

# 프로필 보기 뷰
class ProfileView(LoginRequiredMixin, DetailView):
    model = User  # User 모델을 사용
    template_name = "accounts/profile.html"  # 사용할 템플릿 파일 경로

    def get_object(self, queryset=None):
        # 현재 로그인한 사용자의 프로필 정보를 반환
        return self.request.user

# 프로필 수정 뷰
class ProfileUpdateView(LoginRequiredMixin, UpdateView):
    model = User  # User 모델을 사용
    form_class = ProfileUpdateForm  # 프로필 수정 폼 클래스
    template_name = "accounts/profile_update.html"  # 사용할 템플릿 파일 경로
    success_url = reverse_lazy('accounts:profile')  # 수정 성공 시 프로필 페이지로 리다이렉트

    def get_object(self, queryset=None):
        # 현재 로그인한 사용자의 정보를 수정
        return self.request.user

# 비밀번호 변경 뷰
class ChangePasswordView(LoginRequiredMixin, PasswordChangeView):
    form_class = CustomPasswordChangeForm  # 비밀번호 변경 폼 클래스
    template_name = "accounts/change_password.html"  # 사용할 템플릿 파일 경로
    success_url = reverse_lazy('accounts:profile')  # 변경 성공 시 프로필 페이지로 리다이렉트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from django.urls import path
from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
    )
from .views import (
    SignupAPIView,
    ProfileAPIView,
    ProfileUpdateAPIView,
    ChangePasswordAPIView,

    SignupView, 
    UserLoginView, 
    UserLogoutView, 
    ProfileView, 
    ProfileUpdateView, 
    ChangePasswordView,
    )

app_name = "accounts"

urlpatterns = [
    path("signup/", SignupView.as_view(), name="signup"),
    path("login/", UserLoginView.as_view(), name="login"),
    path("logout/", UserLogoutView.as_view(), name="logout"),
    path("profile/", ProfileView.as_view(), name="profile"),
    path("profile-update/", ProfileUpdateView.as_view(), name="profile_update"),
    path("change-password/", ChangePasswordView.as_view(), name="change_password"),
]

urlpatterns += [
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    path('api/signup/', SignupAPIView.as_view(), name='api_signup'),
    path('api/profile/', ProfileAPIView.as_view(), name='api_profile'),
    path('api/profile/update/', ProfileUpdateAPIView.as_view(), name='api_profile_update'),
    path('api/change-password/', ChangePasswordAPIView.as_view(), name='api_change_password'),
]

MySQL 연동

  1. MySQL 및 MySQL-shell 설치

    1
    2
    
    scoop install mysql
    scoop install mysql-shell
    
  2. MySQL 서버 실행
    • 콘솔에서 실행(서버 상태를 확인 가능):

      1
      
       mysqld --console
      
    • 백그라운드 실행(콘솔 종료와 무관):

      1
      
       mysqld --standalone
      
  3. MySQL 서버 상태 확인

    1
    
    mysqladmin -u root -p version
    
  4. MySQL 서버 종료

    1
    
    Ctrl + C
    

MySQL 드라이버 설치

  1. MySQL 드라이버 설치

    1
    
    pip install mysqlclient
    
    • Windows에서 mysqlclient 설치가 실패하면 대신 pymysql을 사용할 수 있습니다:

      1
      
       pip install pymysql
      
1
2
import pymysql
pymysql.install_as_MySQLdb()
  1. 환경 변수를 더 간편하게 관리하기 위한 python-decouple 라이브러리 사용
    • 루트 프로젝트 폴더 .env 파일 생성해서 관리
    1
    
    pip install python-decouple
    
  2. requirements.txt 업데이트

    1
    
    pip freeze > requirements.txt
    

DB, 사용자 생성 및 권한 부여

  • MySQL 서버 실행 후 진행하기
  • MySQL 접속 (초기 비번 없어서 명령 후 입력 후 비번 입력창은 Enter)

    1
    
     mysql -u root -p
    
  • MySQL 연동할 데이터 베이스 생성

    1
    
     CREATE DATABASE <DB_name> CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
    
  • 사용자 생성 및 비번 설정

    1
    
     CREATE USER 'user'@'localhost' IDENTIFIED BY 'user_password';
    
  • 사용자 권한 부여

    1
    
     GRANT ALL PRIVILEGES ON <DB_name>.* TO 'user'@'localhost';
    
  • 권한 테이블의 변경 내용을 즉시 적용하기 위한 명령어

    1
    
     FLUSH PRIVILEGES;
    
  • 사용자 권한 확인

    1
    
     SHOW GRANTS FOR 'user'@'localhost';
    
  • mysql 쉘에서 나오기

    1
    
     exit
    
  • 사용자 권한 확인

    1
    
     mysql -u user -p
    
    • 비번 입력 후 쉘 접속

      1
      
       USE <DB_name>;
      
  • 확인 후 mysql 쉘에서 나오기

    1
    
     exit
    

SQLite에서 MySQL로 데이터 이전하는 법. 1

  1. SQLite 데이터베이스에서 데이터 덤프
    • 시스템 테이블 제외하고 전체 데이터 덤프:

      1
      
       python manage.py dumpdata --exclude auth.permission --exclude contenttypes > data.json
      

Django 프로젝트의 데이터베이스 설정 변경

  • 기존 settings.py (SQLite)

    1
    2
    3
    4
    5
    6
    7
    
     DATABASES = {
        'default': {
           'ENGINE': 'django.db.backends.sqlite3',
           'NAME': BASE_DIR / 'db.sqlite3',
        }
     }
    
  • 프로젝트 루트 경로에 .env 파일 생성

    1
    2
    3
    4
    5
    
     DB_NAME=<DB_name>
     DB_USER=user
     DB_PASSWORD=user_password
     DB_HOST=127.0.0.1
     DB_PORT=3306
    
  • 수정된 settings.py (MySQL)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
     from decouple import config
    
     DATABASES = {
        'default': {
           'ENGINE': 'django.db.backends.mysql',  # MySQL 백엔드
           'NAME': config('DB_NAME'),             # MySQL 데이터베이스 이름
           'USER': config('DB_USER'),             # MySQL 사용자
           'PASSWORD': config('DB_PASSWORD'),     # MySQL 비밀번호
           'HOST': config('DB_HOST'),             # MySQL 호스트 (로컬)
           'PORT': config('DB_PORT'),             # MySQL 포트 (기본값: 3306)
           'OPTIONS': {
                 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'"
           },
        }
     }
    
    

SQLite에서 MySQL로 데이터 이전하는 법. 2

  1. MySQL 데이터베이스 마이그레이션
    • MySQL 데이터베이스로 마이그레이션 파일을 적용:

      1
      
       python manage.py migrate
      
  2. MySQL로 데이터 로드
    • 덤프한 JSON 데이터를 MySQL로 로드:

      1
      
       python manage.py loaddata data.json
      

중요 참고사항

  1. 중복 데이터 문제 해결
    • django_content_type 테이블과 같은 시스템 데이터가 중복될 수 있습니다. MySQL에서 관련 데이터를 초기화하고, 다시 마이그레이션을 실행하세요:

      1
      
       DELETE FROM django_content_type;
      
  2. 관리자 계정 생성
    • 데이터 로드 후 createsuperuser 명령으로 관리자 계정을 생성하세요. 데이터를 이전했다면 생략해도 됩니다.

      1
      
       python manage.py createsuperuser
      
  3. 데이터베이스 연결 확인
    • Django에서 MySQL 연결 상태를 확인하려면:

      1
      
       python manage.py dbshell
      
This post is licensed under CC BY 4.0 by the author.