회원가입 링크 추가
templates\common\login.html
...
<div class="container my-3">
<div class="row">
<div class="col-4">
<h4>로그인</h4>
</div>
<div class="col-8 text-right">
<span>
또는 <a href="{% url 'common:signup' %}">계정을 만드세요</a>
</span>
</div>
</div>
<form method="post" action="{% url 'common:login' %}">
...
회원가입 url 매핑 추가
common\urls.py
...
from . import views
...
urlpatterns = [
...
path('signup/', views.signup, name='signup'),
]
...
회원가입에 사용할 폼 생성
common\forms.py 생성
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
class UserForm(UserCreationForm):
email = forms.EmailField(label="이메일")
class Meta:
model = User
fields = ("username", "email")
| 속성명 | 설명 |
|---|---|
| username | 사용자이름 |
| password1 | 비밀번호1 |
| password2 | 비밀번호2 (비밀번호1을 제대로 입력했는지 대조하기 위한 값) |
회원가입을 위한 signup 함수 정의
common\views.py
from django.contrib.auth import authenticate, login
from django.shortcuts import render, redirect
from common.forms import UserForm
# Create your views here.
def signup(request):
"""
회원가입
"""
if request.method == "POST":
form = UserForm(request.POST)
if form.is_valid():
form.save()
username = form.cleaned_data.get('username')
raw_password = form.cleaned_data.get("password1")
user = authenticate(username=username, password=raw_password)
login(request, user)
return redirect('index')
else:
form = UserForm()
return render(request, 'common/signup.html', {'form': form})
request.POST : django.http.request.QueryDict 객체{'username': 'alice', 'password1': '...','password2':'...'} 등)request.POST.copy()로 복사해서 수정함request.POST 는 UserForm 에 무엇을 전달?UserForm(request.POST) 에 넘기면, 이 데이터로 폼이 “바운드(bound)” 됨is_valid() 호출 시, 각 필드(username, password 등) 에 값을 채워 넣고 유효성 검사를 수행할 수 있게 된다cleaned_data에서 값을 가져오는가?form.is_valid() 가 통과되면, 폼은 원시 입력값을 검증/정규화 하여 cleaned_data 에 담는다request.POST 는 검증 전 사용자 입력 그대로라 신뢰할 수 없음cleaned_data 는 검증(Validation) + 정제(Cleaning) 를 거친 안전한 값username, password1 등은 form.cleaned_data[...] 로 꺼낸다.authenticate()authenticate(request=None, username=None, password=None, **credentials) (백엔드에 따라 이메일 등 다른 자격으로도 인증 가능)User 인스턴스 (그리고 user.backend 속성에 사용된 백엔드 경로가 세팅됨)Nonerequest 를 넘기면 일부 백엔드에서 추가 컨텍스트(IP, 세션 등)를 활용 가능.login()login(request, user, backend=None)request.user 가 해당 유저로 설정Noneauthenticate() 로 받은 user 는 보통 user.backend 가 설정되어 있어 backend 인자를 별도로 줄 필요가 없습니다.'index' 인 경로로 리다이렉트urls.py 의 name='index' 와 매칭{{ form }} 로 필드/라벨/헬프텍스트/에러 등을 출력하므로 폼 인스턴스가 필요POST & 유효성 실패 시: form = UserForm(request.POST) 로 바운드된 폼이 그대로 render(...) 에 전달되어 사용자 입력 + 에러 메시지가 화면에 표시됨GET (처음 진입): form = UserForm() 으로 언바운드(비어있는) 폼을 만들어 빈 입력창을 렌더링GET 분기에서 생성한 UserForm() 은 데이터가 없어 “언바운드” 상태(필드 정의/위젯/라벨/헬프텍스트 등 메타 정보는 포함).UserForm(initial={'username': 'guest'}) 처럼 초기값을 줄 수도 있다회원가입 템플릿 생성
templates\common\signup.html 생성
{% extends "base.html" %}
{% block content %}
<div class="container my-3">
<div class="row my-3">
<div class="col-4">
<h4>회원가입</h4>
</div>
<div class="col-8 text-right">
<span>
또는 <a href="{% url 'common:login' %}">로그인 하세요.</a>
</span>
</div>
</div>
<form method="post" class="post-form">
{% csrf_token %}
<div class="form-group">
<label for="username">사용자 이름</label>
<input type="text" class="form-control" name="username" id="username"
value="{{ form.username.value|default_if_none:'' }}">
</div>
<div class="form-group">
<label for="password1">비밀번호</label>
<input type="password" class="form-control" name="password1" id="password1"
value="{{ form.password1.value|default_if_none:'' }}">
</div>
<div class="form-group">
<label for="password2">비밀번호 확인</label>
<input type="password" class="form-control" name="password2" id="password2"
value="{{ form.password2.value|default_if_none:'' }}">
</div>
<div class="form-group">
<label for="email">이메일</label>
<input type="text" class="form-control" name="email" id="email"
value="{{ form.email.value|default_if_none:'' }}">
</div>
<button type="submit" class="btn btn-primary">생성하기</button>
</form>
</div>
{% endblock %}
django.contrib.auth.models.User의 기본 필드는 아래와 같음
이미 완성된 고정(User) 모델이며 Django 내부 인증 시스템에서 바로 사용할 수 있지만,
구조를 직접 바꾸거나 필드를 추가할 수 없음
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | AutoField | 기본 키 (자동 생성) |
| password | CharField | 해시된 비밀번호 저장 |
| last_login | DateTimeField | 마지막 로그인 시각 |
| is_superuser | BooleanField | 관리자 권한 여부 |
| username | CharField | 사용자 이름 (기본 로그인 ID) |
| first_name | CharField | 이름(이름 부분) |
| last_name | CharField | 성(성 부분) |
| EmailField | 이메일 주소 | |
| is_staff | BooleanField | 관리자 페이지 접근 가능 여부 |
| is_active | BooleanField | 계정 활성화 여부 |
| date_joined | DateTimeField | 가입 일자 |
이 모델은 장고 내부적으로 완전히 고정되어 있어서, 원본 User를 수정하려면 "완전히 새 모델을 만들어야" 함
기본 User의 모든 필드를 그대로 상속받되, 추가 필드를 마음대로 덧붙일 수 있게 설계된 "추상 클래스"
from django.contrib.auth.models import AbstractUser
from django.db import models
class CustomUser(AbstractUser):
phone = models.CharField(max_length=20, blank=True)
address = models.CharField(max_length=255, blank=True)
nickname = models.CharField(max_length=30, blank=True)
이렇게 하면 결과적으로 User + 추가 필드들이 모두 포함된 완성형 유저 모델이 됨
이런 경우엔 AbstractUser를 상속한 CustomUser를 만들어야 함
즉,
필드만 추가하고 싶다면 → AbstractUser
인증 로직도 직접 만들고 싶다면 → AbstractBaseUser
AbstractUser 클래스 상속하여 CustomUser 생성
common\models.py
from django.db import models
from django.contrib.auth.models import AbstractUser
# Create your models here.
class CustomUser(AbstractUser):
phone = models.CharField(max_length=20, blank=True)
address = models.CharField(max_length=255, blank=True)
nickname = models.CharField(max_length=30, blank=True)
def __str__(self):
return self.username
추가할 필드들 선언 후 추가
common\forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm
from .models import CustomUser
class UserForm(UserCreationForm):
email = forms.EmailField(label="이메일")
phone = forms.CharField(required=False, label="전화번호")
address = forms.CharField(required=False, label="주소")
nickname = forms.CharField(required=False, label="닉네임")
class Meta:
model = CustomUser
fields = ("username", "email", "phone", "address", "nickname")
추가한 필드 html에도 추가
templates\common\signup.html
{% extends "base.html" %}
{% block content %}
<div class="container my-3">
<div class="row my-3">
<div class="col-4">
<h4>회원가입</h4>
</div>
<div class="col-8 text-right">
<span>
또는 <a href="{% url 'common:login' %}">로그인 하세요.</a>
</span>
</div>
</div>
<form method="post" class="post-form">
{% csrf_token %}
{% include "form_errors.html" %}
<div class="form-group">
<label for="username">사용자 이름</label>
<input type="text" class="form-control" name="username" id="username"
value="{{ form.username.value|default_if_none:'' }}">
</div>
<div class="form-group">
<label for="password1">비밀번호</label>
<input type="password" class="form-control" name="password1" id="password1"
value="{{ form.password1.value|default_if_none:'' }}">
</div>
<div class="form-group">
<label for="password2">비밀번호 확인</label>
<input type="password" class="form-control" name="password2" id="password2"
value="{{ form.password2.value|default_if_none:'' }}">
</div>
<div class="form-group">
<label for="email">이메일</label>
<input type="text" class="form-control" name="email" id="email"
value="{{ form.email.value|default_if_none:'' }}">
</div>
<div class="form-group">
<label for="phone">전화번호</label>
<input type="tel" class="form-control" name="phone" id="phone"
value="{{ form.phone.value|default_if_none:'' }}">
</div>
<div class="form-group">
<label for="address">주소</label>
<input type="text" class="form-control" name="address" id="address"
value="{{ form.address.value|default_if_none:'' }}">
</div>
<div class="form-group">
<label for="nickname">닉네임</label>
<input type="text" class="form-control" name="nickname" id="nickname"
value="{{ form.nickname.value|default_if_none:'' }}">
</div>
<button type="submit" class="btn btn-primary">생성하기</button>
</form>
</div>
{% endblock %}
Admin 페이지에서 생성된 사용자 확인 가능하도록 설정
pybo\admin.py
from django.contrib import admin
from .models import Question
from .models import Answer
# Register your models here.
class QuestionAdmin(admin.ModelAdmin):
search_fields = ['subject']
admin.site.register(Question, QuestionAdmin)
admin.site.register(Answer)
migration 충돌 해결
config\settings.py
AUTH_USER_MODEL = 'common.CustomUser'
migrations 폴더에서 init.py 빼고 다 삭제
python manage.py makemigrations
python manage.py migrate
pybo\models.py
...
from common.models import CustomUser
class Question(models.Model):
author = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
...
class Answer(models.Model):
author = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
...
python [manage.py](http://manage.py) makemigrations 실행
It is impossible to add a non-nullable field 'author' to question without specifying a default. This is because the database needs something to populate existing rows.
Please select a fix:
1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
2) Quit and manually define a default value in models.py.
Select an option:
해당 메세지 출력
author 필드를 추가하면 이미 등록되어 있던 게시물에 author 필드에 해당되는 값이 저장되어야 하는데, 장고는 author 필드에 어떤 값을 넣어야 하는지 모르기 때문
그래서 장고가 기존에 저장된 Question, Answer 모델 데이터에는 author 필드값으로 어떤 값을 저장해야 하는지 묻는 것
1 → author 필드를 null 로 설정
2 → author 필드 값에 강제로 임의 계정 정보를 추가
1 선택
python [manage.py](http://manage.py) migrate 실행
답변 등록, 질문 등록 함수 수정
pybo\views.py
def answer_create(request, question_id):
...
if form.is_valid():
answer = form.save(commit=False)
answer.author = request.user
...
def question_create(request):
...
if form.is_valid():
question = form.save(commit=False)
question.author = request.user
...
하지만 로그아웃 상태에서 질문 또는 답변을 등록하면 다음과 같은 ValueError가 발생한다.

이 오류는 request.user가 User 객체가 아닌 AnonymousUser 객체라서 발생한 것이다. 조금 더 자세히 설명하자면 request.user에는 로그아웃 상태이면 AnonymousUser 객체가, 로그인 상태이면 User 객체가 들어있는데, 앞에서 우리는 author 속성을 정의할 때 User를 이용하도록 했다. 그래서 answer.author = request.user에서 User 대신 AnonymousUser가 대입되어 오류가 발생한 것이다.
이 문제를 해결하려면 request.user를 사용하는 함수에 @login_required 애너테이션을 사용해야 한다. @login_required 애너테이션이 붙은 함수는 로그인이 필요한 함수를 의미한다.
로그인이 필요한 함수에 @login_required 애노테이션 적용
pybo\views.py
from django.contrib.auth.decorators import login_required
...
@login_required(login_url='common:login')
def answer_create(request, question_id):
...
@login_required(login_url='common:login')
def question_create(request):
...
answer_create 함수와 question_create 함수는 함수내에서 request.user를 사용하므로 로그인이 필요한 함수이다. 따라서 위와 같이 @login_required 어노테이션을 사용해야 한다.
로그아웃 상태에서 @login_required 어노테이션이 적용된 함수가 호출되면 자동으로 로그인 화면으로 이동하게 된다. @login_required 어노테이션은 login_url='common:login' 처럼 로그인 URL을 지정할 수 있다.
이렇게 수정한후 로그아웃 상태에서 질문을 등록하거나 답변을 등록하면 자동으로 로그인 화면으로 이동되는 것을 확인할 수 있을 것이다.
그런데 로그아웃 상태에서 '질문 등록하기'를 눌러 로그인 화면으로 전환된 상태에서 웹 브라우저 주소창의 URL을 보면 next 파라미터가 있을 것이다.

이는 로그인 성공 후 next 파라미터에 있는 URL로 페이지를 이동하겠다는 의미이다. 그런데 지금은 그렇게 되고 있지 않다. 로그인 후 next 파라미터에 있는 URL로 페이지를 이동하려면 로그인 템플릿에 다음과 같이 hidden 타입의 next 항목을 추가해야 한다.
로그인 템플릿에 hidden 항목 추가하여 next 파라미터 활용
templates\common\login.html
(... 생략 ...)
<form method="post" action="{% url 'common:login' %}">
{% csrf_token %}
<input type="hidden" name="next" value="{{ next }}"> <!-- 로그인 성공후 이동되는 URL -->
{% include "form_errors.html" %}
(... 생략 ...)
로그아웃 상태에서 아예 글을 작성할 수 없게 하기
templates\pybo\question_detail.html
...
<div class="form-group">
<textarea name="content" id="content"
{% if not user.is_authenticated %}disabled{% endif %}
class="form-control" rows="10"></textarea>
</div>
<input type="submit" value="답변 등록" class="btn btn-primary">
...
질문 목록 템플릿 수정
templates\pybo\question_list.html
<tr class="text-center">
<td>
...
</td>
<td class="text-left">
<a href="{% url 'pybo:detail' question.id %}">
{{ question.subject }}
</a>
{% if question.answer_set.count > 0 %}
<span class="text-danger small mx-2">{{ question.answer_set.count }}</span>
{% endif %}
</td>
<td>{{ question.author.username }}</td> <!-- 글쓴이 추가 -->
<td>{{ qustion.create_date }}</td>
</tr>
질문 상세 템플릿 수정
templates\pybo\question_detail.html
ed!(... 생략 ...)
<!-- 질문 -->
<h2 class="border-bottom py-2">{{ question.subject }}</h2>
<div class="card-body">
<div class="card-text" style="white-space: pre-line;">{{ question.content }}</div>
<div class="d-flex justify-content-end">
<div class="badge bg-light text-dark p-2 text-start">
<div class="mb-2">{{ question.author.username }}</div>
<div>{{ question.create_date }}</div>
</div>
</div>
</div>
(... 생략 ...)
Question, Answer 모델에 modify_date 필드 추가
modify_date = models.DateTimeField(null=True, blank=True)
form.is_valid() 를 통한 입력 폼 데이터 유효성 검사 시 값이 없어도 되도록
null=Trueblank=Truemakemigrations, migrate
질문 수정 버튼 추가
templates\pybo\question_detail.html
<!-- 질문 내용 -->
<div class="card my-3">
<div class="card-body">
<div class="card-text" style="white-space: pre-line;">
{{ question.content }}
</div>
<div class="d-flex justify-content-end">
<div class="badge badge-light p-2 text-left">
<div class="mb-2">{{ question.author.username }}</div>
<div>{{ question.create_date }}</div>
</div>
</div>
{% if request.user == question.author %}
<div class="my-3">
<a href="{% url 'pybo:question_modify' question.id %}" class="btn btn-sm btn-outline-secondary">수정</a>
</div>
{% endif %}
</div>
</div>
질문 수정 버튼의 url 매핑 추가
pybo\urls.py
urlpatterns = [
...
path('question/modify/<int:question_id>/', views.question_modify, name='question_modify'),
]
질문 수정 함수 추가
pybo\views.py
from django.contrib import messages
...
@login_required(login_url='common:login')
def question_modify(request, question_id):
"""
pybo 질문 수정
"""
question = get_object_or_404(Question, pk=question_id)
if request.user != question.author:
messages.error(request, '수정권한이 없습니다')
return redirect('pybo:detail', question_id=question_id)
if request.method == 'POST':
form = QuestionForm(request.POST, instance=question)
if form.is_valid():
question = form.save(commit=False)
question.author = request.user
question.modify_date = timezone.now()
question.save()
return redirect('pybo:detail', question_id=question_id)
else:
form = QuestionForm(instance=question)
context = {'form': form}
return render(request, 'pybo/question_form.html', context)
질문 삭제 버튼 추가
templates\pybo\question_detail.html
{% if request.user == question.author %}
<div class="my-3">
<a href="{% url 'pybo:question_modify' question.id %}" class="btn btn-sm btn-outline-secondary">수정</a>
<a href="#" class="delete btn btn-sm btn-outline-secondary"
data-uri="{% url 'pybo:question_delete' question.id %}">삭제</a>
</div>
{% endif %}
질문 삭제 버튼에 jQuery 사용
jQuery 실행을 위해 base.html 파일 수정
templates\base.html
...
<script src="{% static 'jquery-3.7.1.min.js' %}"></script>
<script src="{% static 'bootstrap.min.js' %}"></script>
<!-- 자바스크립트 start -->
{% block script %}
{% endblock %}
<!-- 자바스크립트 end -->
</body>
자바 스크립트 블록 삽입 가능
질문 템플릿에 삭제 알림 창 기능 추가(jQuery 이용)
templates\pybo\question_detail.html
...(맨끝)
{% block script %}
<script type="text/javascript">
$(document).ready(function(){
$(".delete").on('click', function() {
if(confirm("정말로 삭제하시겠습니까?")) {
location.href = $(this).data('uri');
}
});
});
</script>
{% endblock %}
질문 삭제 url 매핑 추가
pybo\urls.py
path('question/delete/<int:question_id>/', views.question_delete, name='question_delete'),
질문 삭제 함수 추가
pybo\views.py
@login_required(login_url='common:login')
def question_delete(request, question_id):
"""
질문 삭제
"""
question = get_object_or_404(Question, pk=question_id)
if request.user != question.author:
messages.error(request, '삭제권한이 없습니다')
return redirect('pybo:detail', question_id=question_id)
question.delete()
return redirect('pybo:index')
messages.error(request, '삭제권한이 없습니다')django.contrib.messages)은 “사용자에게 일시적인 알림 메시지(예: 성공/에러 메시지)”를 요청(request) 단위로 저장하는 구조 즉,를 저장 그래서 메시지를 저장하려면 “이 요청이 누구의 요청인지” 알아야 하고, 그 정보를"현재 요청을 처리하는 사용자에게만 보여줄 임시 메시지"
request 객체로 전달해야 함