CBV 사용한 DRF, MySQL 연동
클래스 기반 뷰(CBV)를 사용하여 Django Rest Framework(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 연동
MySQL 및 MySQL-shell 설치
1 2
scoop install mysql scoop install mysql-shell
- MySQL 서버 실행
콘솔에서 실행(서버 상태를 확인 가능):
1
mysqld --console
백그라운드 실행(콘솔 종료와 무관):
1
mysqld --standalone
MySQL 서버 상태 확인
1
mysqladmin -u root -p version
MySQL 서버 종료
1
Ctrl + C
MySQL 드라이버 설치
MySQL 드라이버 설치
1
pip install mysqlclient
Windows에서
mysqlclient
설치가 실패하면 대신pymysql
을 사용할 수 있습니다:1
pip install pymysql
1
2
import pymysql
pymysql.install_as_MySQLdb()
- 환경 변수를 더 간편하게 관리하기 위한
python-decouple
라이브러리 사용- 루트 프로젝트 폴더
.env
파일 생성해서 관리
1
pip install python-decouple
- 루트 프로젝트 폴더
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
- 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
- MySQL 데이터베이스 마이그레이션
MySQL 데이터베이스로 마이그레이션 파일을 적용:
1
python manage.py migrate
- MySQL로 데이터 로드
덤프한 JSON 데이터를 MySQL로 로드:
1
python manage.py loaddata data.json
중요 참고사항
- 중복 데이터 문제 해결
django_content_type
테이블과 같은 시스템 데이터가 중복될 수 있습니다. MySQL에서 관련 데이터를 초기화하고, 다시 마이그레이션을 실행하세요:1
DELETE FROM django_content_type;
- 관리자 계정 생성
데이터 로드 후
createsuperuser
명령으로 관리자 계정을 생성하세요. 데이터를 이전했다면 생략해도 됩니다.1
python manage.py createsuperuser
- 데이터베이스 연결 확인
Django에서 MySQL 연결 상태를 확인하려면:
1
python manage.py dbshell